mirror of
https://github.com/microsoft/mcp.git
synced 2026-11-04 12:01:31 +00:00
Uvx support (#1359)
* add uvx support * updates * update * update * update * modifications to include the new artifact * update * update * added pypi settings * update release-pypi script * address feedback * ping build tool versions --------- Co-authored-by: Scott Beddall <scbedd@microsoft.com>
This commit is contained in:
1 parent
a66acba42b
commit
023bfd6ccb
13 files changed
+813
-3
No files matched your search
@@ -0,0 +1,39 @@
|
||||
parameters:
|
||||
- name: DependsOn
|
||||
type: object
|
||||
|
||||
jobs:
|
||||
- job: PackPyPI
|
||||
displayName: "Pack PyPI Package"
|
||||
dependsOn: ${{ parameters.DependsOn }}
|
||||
timeoutInMinutes: 120
|
||||
steps:
|
||||
- checkout: self
|
||||
|
||||
- task: UsePythonVersion@0
|
||||
displayName: Use Python 3.12
|
||||
inputs:
|
||||
versionSpec: '3.12'
|
||||
|
||||
- download: current
|
||||
displayName: Download build_info
|
||||
artifact: build_info
|
||||
|
||||
- download: current
|
||||
displayName: Download binaries_signed
|
||||
artifact: binaries_signed
|
||||
|
||||
- task: Powershell@2
|
||||
displayName: "Pack modules for PyPI"
|
||||
inputs:
|
||||
pwsh: true
|
||||
filePath: $(Build.SourcesDirectory)/eng/scripts/Pack-Pypi.ps1
|
||||
arguments: >
|
||||
-BuildInfoPath '$(Pipeline.Workspace)/build_info/build_info.json'
|
||||
-ArtifactsPath '$(Pipeline.Workspace)/binaries_signed'
|
||||
-OutputPath '$(Build.ArtifactStagingDirectory)'
|
||||
|
||||
- template: /eng/common/pipelines/templates/steps/publish-1es-artifact.yml
|
||||
parameters:
|
||||
ArtifactPath: $(Build.ArtifactStagingDirectory)
|
||||
ArtifactName: packages_pypi
|
||||
@@ -0,0 +1,82 @@
|
||||
parameters:
|
||||
- name: ServerName
|
||||
type: string
|
||||
- name: IncludeNative
|
||||
type: boolean
|
||||
- name: DependsOn
|
||||
type: object
|
||||
|
||||
jobs:
|
||||
- deployment: PublishPyPI
|
||||
displayName: "Publish to PyPI"
|
||||
dependsOn: ${{ parameters.DependsOn }}
|
||||
condition: and(succeeded(), ne(variables['Skip.PublishPackage'], 'true'))
|
||||
templateContext:
|
||||
type: releaseJob
|
||||
isProduction: true
|
||||
inputs:
|
||||
- input: pipelineArtifact
|
||||
artifactName: packages_pypi
|
||||
targetPath: $(Pipeline.Workspace)/drop
|
||||
environment: package-publish
|
||||
timeoutInMinutes: 120
|
||||
pool:
|
||||
name: azsdk-pool
|
||||
image: ubuntu-24.04
|
||||
os: linux
|
||||
strategy:
|
||||
runOnce:
|
||||
deploy:
|
||||
steps:
|
||||
- pwsh: |
|
||||
$esrpDirectory = "$(Pipeline.Workspace)/esrp-release/${{ parameters.ServerName }}"
|
||||
New-Item -ItemType Directory -Force -Path $esrpDirectory
|
||||
Get-ChildItem -Path "$(Pipeline.Workspace)/drop/${{ parameters.ServerName }}" `
|
||||
| Where-Object { ($_.Name -like "*.tar.gz" -or $_.Name -like "*.whl") } `
|
||||
| Copy-Item -Destination $esrpDirectory
|
||||
Get-ChildItem $esrpDirectory
|
||||
displayName: Isolate files for ESRP Publish
|
||||
|
||||
- task: EsrpRelease@9
|
||||
displayName: 'Publish to ESRP'
|
||||
inputs:
|
||||
ConnectedServiceName: 'Azure SDK PME Managed Identity'
|
||||
ClientId: '5f81938c-2544-4f1f-9251-dd9de5b8a81b'
|
||||
DomainTenantId: '975f013f-7f24-47e8-a7d3-abc4752bf346'
|
||||
UseManagedIdentity: true
|
||||
KeyVaultName: 'kv-azuresdk-codesign'
|
||||
SignCertName: 'azure-sdk-esrp-release-certificate'
|
||||
Intent: 'PackageDistribution'
|
||||
ContentType: 'PyPI'
|
||||
FolderLocation: $(Pipeline.Workspace)/esrp-release/${{ parameters.ServerName }}
|
||||
Owners: ${{ coalesce(variables['Build.RequestedForEmail'], 'azuresdk@microsoft.com') }}
|
||||
Approvers: ${{ coalesce(variables['Build.RequestedForEmail'], 'azuresdk@microsoft.com') }}
|
||||
ServiceEndpointUrl: 'https://api.esrp.microsoft.com'
|
||||
MainPublisher: 'ESRPRELPACMANTEST'
|
||||
|
||||
- ${{ if eq(parameters.IncludeNative, true) }}:
|
||||
- pwsh: |
|
||||
$esrpDirectory = "$(Pipeline.Workspace)/esrp-release/${{ parameters.ServerName }}-native"
|
||||
New-Item -ItemType Directory -Force -Path $esrpDirectory
|
||||
Get-ChildItem -Path "$(Pipeline.Workspace)/drop/${{ parameters.ServerName }}-native" `
|
||||
| Where-Object { ($_.Name -like "*.tar.gz" -or $_.Name -like "*.whl") } `
|
||||
| Copy-Item -Destination $esrpDirectory
|
||||
Get-ChildItem $esrpDirectory
|
||||
displayName: Isolate native files for ESRP Publish
|
||||
|
||||
- task: EsrpRelease@9
|
||||
displayName: 'Publish native package to ESRP'
|
||||
inputs:
|
||||
ConnectedServiceName: 'Azure SDK PME Managed Identity'
|
||||
ClientId: '5f81938c-2544-4f1f-9251-dd9de5b8a81b'
|
||||
DomainTenantId: '975f013f-7f24-47e8-a7d3-abc4752bf346'
|
||||
UseManagedIdentity: true
|
||||
KeyVaultName: 'kv-azuresdk-codesign'
|
||||
SignCertName: 'azure-sdk-esrp-release-certificate'
|
||||
Intent: 'PackageDistribution'
|
||||
ContentType: 'PyPI'
|
||||
FolderLocation: $(Pipeline.Workspace)/esrp-release/${{ parameters.ServerName }}-native
|
||||
Owners: ${{ coalesce(variables['Build.RequestedForEmail'], 'azuresdk@microsoft.com') }}
|
||||
Approvers: ${{ coalesce(variables['Build.RequestedForEmail'], 'azuresdk@microsoft.com') }}
|
||||
ServiceEndpointUrl: 'https://api.esrp.microsoft.com'
|
||||
MainPublisher: 'ESRPRELPACMANTEST'
|
||||
@@ -55,6 +55,12 @@ jobs:
|
||||
IncludeNative: ${{ parameters.IncludeNative }}
|
||||
DependsOn: TagRepository
|
||||
|
||||
- template: /eng/pipelines/templates/jobs/pypi/release-pypi.yml
|
||||
parameters:
|
||||
ServerName: ${{ parameters.ServerName }}
|
||||
IncludeNative: ${{ parameters.IncludeNative }}
|
||||
DependsOn: TagRepository
|
||||
|
||||
- template: /eng/pipelines/templates/jobs/nuget/release-nuget.yml
|
||||
parameters:
|
||||
ServerName: ${{ parameters.ServerName }}
|
||||
|
||||
@@ -110,6 +110,10 @@ jobs:
|
||||
parameters:
|
||||
DependsOn: Sign
|
||||
|
||||
- template: /eng/pipelines/templates/jobs/pypi/pack-pypi.yml
|
||||
parameters:
|
||||
DependsOn: Sign
|
||||
|
||||
- template: /eng/pipelines/templates/jobs/nuget/pack-and-sign-nuget.yml
|
||||
parameters:
|
||||
DependsOn: Sign
|
||||
|
||||
@@ -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
|
||||
@@ -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()
|
||||
@@ -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",
|
||||
]
|
||||
@@ -54,6 +54,10 @@ $propertyList = @(
|
||||
'DnxDescription',
|
||||
'DnxToolCommandName',
|
||||
|
||||
'PypiPackageName',
|
||||
'PypiPackageKeywords',
|
||||
'PypiDescription',
|
||||
|
||||
'IsAotCompatible',
|
||||
|
||||
'McpRepositoryName'
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,341 @@
|
||||
#!/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.
|
||||
|
||||
.EXAMPLE
|
||||
./Pack-Pypi.ps1
|
||||
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
|
||||
)
|
||||
|
||||
$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 Get-PythonCommand {
|
||||
# Try python3 first (common on Linux/macOS), but verify it works
|
||||
# On Windows, "python3" may be a Store alias that doesn't work
|
||||
try {
|
||||
$null = & python3 --version 2>&1
|
||||
if ($LASTEXITCODE -eq 0) {
|
||||
return "python3"
|
||||
}
|
||||
} catch {}
|
||||
|
||||
# Fall back to python
|
||||
try {
|
||||
$null = & python --version 2>&1
|
||||
if ($LASTEXITCODE -eq 0) {
|
||||
return "python"
|
||||
}
|
||||
} catch {}
|
||||
|
||||
throw "Python is not installed or not in PATH. Please install Python 3.10+."
|
||||
}
|
||||
|
||||
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, skip servers without PyPI configuration
|
||||
$basePackageName = if ([string]::IsNullOrWhiteSpace($server.pypiPackageName)) { $null } else { $server.pypiPackageName }
|
||||
if (!$basePackageName) {
|
||||
Write-Host "Skipping $($server.name) - no PyPI package name configured" -ForegroundColor Yellow
|
||||
return
|
||||
}
|
||||
|
||||
$description = if ([string]::IsNullOrWhiteSpace($server.pypiDescription)) { $server.description } else { $server.pypiDescription }
|
||||
$cliName = $server.cliName
|
||||
$keywords = @(if ($server.pypiPackageKeywords) { $server.pypiPackageKeywords } else { $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 = Get-PythonCommand
|
||||
|
||||
Invoke-LoggedCommand "$pythonCmd -m pip install --quiet build==1.2.2 wheel==0.45.1"
|
||||
|
||||
# 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-<platform>.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
|
||||
}
|
||||
|
||||
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
|
||||
@@ -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 = @{}
|
||||
)
|
||||
|
||||
@@ -23,6 +23,10 @@
|
||||
<DnxPackageId>Azure.Mcp</DnxPackageId>
|
||||
<DnxToolCommandName>$(CliName)</DnxToolCommandName>
|
||||
<DnxPackageTags>azure,mcp</DnxPackageTags>
|
||||
|
||||
<!-- pypi settings -->
|
||||
<PypiPackageName>msmcp-azure</PypiPackageName>
|
||||
<PypiPackageKeywords>azure,mcp,model-context-protocol,ai,llm</PypiPackageKeywords>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup>
|
||||
|
||||
@@ -23,6 +23,10 @@
|
||||
<DnxPackageId>Microsoft.Template.Mcp</DnxPackageId>
|
||||
<DnxToolCommandName>$(CliName)</DnxToolCommandName>
|
||||
<DnxPackageTags>template,mcp</DnxPackageTags>
|
||||
|
||||
<!-- pypi settings -->
|
||||
<PypiPackageName>msmcp-template</PypiPackageName>
|
||||
<PypiPackageKeywords>template,mcp,model-context-protocol,ai,llm</PypiPackageKeywords>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup>
|
||||
|
||||
Reference in new issue
Block a user