diff --git a/eng/common/mcp/README.md b/eng/common/mcp/README.md new file mode 100644 index 000000000..60d929862 --- /dev/null +++ b/eng/common/mcp/README.md @@ -0,0 +1,38 @@ +# Azure SDK MCP Servers + +This document details how to author, publish and use [MCP servers](https://github.com/modelcontextprotocol) for azure sdk team usage. + +## Using the Azure SDK MCP Server + +Run the below command to download and run the azure sdk engsys mcp server manually: + +``` +/eng/common/mcp/azure-sdk-mcp.ps1 -Run +``` + +To install the mcp server for use within vscode copilot agent mode, run the following then launch vscode from the repository root. + +``` +/eng/common/mcp/azure-sdk-mcp.ps1 -UpdateVsCodeConfig +``` + +*When updating the config the script will not overwrite any other server configs.* + +The script will install the latest version of the azsdk cli executable from [tools releases](https://github.com/Azure/azure-sdk-tools/releases) and install it to `$HOME/.azure-sdk-mcp/azsdk`. + +## Authoring an MCP server + +MCP server code should be placed in [azure-sdk-tools/tools/mcp/dotnet](https://github.com/Azure/azure-sdk-tools/tree/main/tools/mcp/dotnet). + +Azure SDK MCP servers should support [stdio and sse transports](https://modelcontextprotocol.io/docs/concepts/transports#server-sent-events-sse). + +When running in copilot the default is stdio mode, but SSE is useful to support for external debugging. + +### Developing MCP servers in C# + +See the [C# MCP SDK](https://github.com/modelcontextprotocol/csharp-sdk) + +Add an [SSE transport](https://github.com/modelcontextprotocol/csharp-sdk/tree/main/samples/AspNetCoreSseServer) + +TODO: Add the azsdk-cli project to pull in MCP server dependencies from the repo + diff --git a/eng/common/mcp/azure-sdk-mcp.ps1 b/eng/common/mcp/azure-sdk-mcp.ps1 new file mode 100755 index 000000000..a344051f7 --- /dev/null +++ b/eng/common/mcp/azure-sdk-mcp.ps1 @@ -0,0 +1,60 @@ +#!/bin/env pwsh + +param( + [string]$FileName = 'azsdk', + [string]$Package = 'azsdk', + [string]$Version, # Default to latest + [string]$InstallDirectory = (Join-Path $HOME ".azure-sdk-mcp" "azsdk"), + [string]$Repository = 'Azure/azure-sdk-tools', + [switch]$Run, + [switch]$UpdateVsCodeConfig, + [switch]$Clean +) + +$ErrorActionPreference = "Stop" + +. (Join-Path $PSScriptRoot '..' 'scripts' 'Helpers' 'AzSdkTool-Helpers.ps1') + +if ($Clean) { + Clear-Directory -Path $InstallDirectory +} + +if ($UpdateVsCodeConfig) { + $vscodeConfigPath = $PSScriptRoot + "../../../.vscode/mcp.json" + if (Test-Path $vscodeConfigPath) { + $vscodeConfig = Get-Content -Raw $vscodeConfig | ConvertFrom-Json -AsHashtable + } + else { + $vscodeConfig = @{} + } + $serverKey = "azure-sdk-mcp" + $serverConfig = @{ + "type" = "stdio" + "command" = "/home/ben/azs/azure-sdk-tools/eng/common/mcp/azure-sdk-mcp.ps1" + } + $orderedServers = [ordered]@{ + $serverKey = $serverConfig + } + if (-not $vscodeConfig.ContainsKey('servers')) { + $vscodeConfig['servers'] = @{} + } + foreach ($key in $vscodeConfig.servers.Keys) { + if ($key -ne $serverKey) { + $orderedServers[$key] = $vscodeConfig.servers[$key] + } + } + $vscodeConfig.servers = $orderedServers + Write-Host "Updating vscode mcp config at $vscodeConfigPath" + $vscodeConfig | ConvertTo-Json -Depth 10 | Set-Content -Path $vscodeConfig -Force +} + +$exe = Install-Standalone-Tool ` + -Version $Version ` + -FileName $FileName ` + -Package $Package ` + -Directory $InstallDirectory ` + -Repository $Repository + +if ($Run) { + Start-Process -FilePath $exe -NoNewWindow -Wait +} \ No newline at end of file diff --git a/eng/common/scripts/Helpers/AzSdkTool-Helpers.ps1 b/eng/common/scripts/Helpers/AzSdkTool-Helpers.ps1 new file mode 100644 index 000000000..4fa981196 --- /dev/null +++ b/eng/common/scripts/Helpers/AzSdkTool-Helpers.ps1 @@ -0,0 +1,194 @@ +Set-StrictMode -Version 4 + +function Get-SystemArchitecture { + $unameOutput = uname -m + switch ($unameOutput) { + "x86_64" { return "X86_64" } + "aarch64" { return "ARM64" } + "arm64" { return "ARM64" } + default { throw "Unable to determine system architecture. uname -m returned $unameOutput." } + } +} + +function Get-Package-Meta( + [Parameter(mandatory = $true)] + $FileName, + [Parameter(mandatory = $true)] + $Package +) { + $ErrorActionPreferenceDefault = $ErrorActionPreference + $ErrorActionPreference = "Stop" + + $AVAILABLE_BINARIES = @{ + "Windows" = @{ + "AMD64" = @{ + "system" = "Windows" + "machine" = "AMD64" + "file_name" = "$FileName-standalone-win-x64.zip" + "executable" = "$Package.exe" + } + } + "Linux" = @{ + "X86_64" = @{ + "system" = "Linux" + "machine" = "X86_64" + "file_name" = "$FileName-standalone-linux-x64.tar.gz" + "executable" = "$Package" + } + "ARM64" = @{ + "system" = "Linux" + "machine" = "ARM64" + "file_name" = "$FileName-standalone-linux-arm64.tar.gz" + "executable" = "$Package" + } + } + "Darwin" = @{ + "X86_64" = @{ + "system" = "Darwin" + "machine" = "X86_64" + "file_name" = "$FileName-standalone-osx-x64.zip" + "executable" = "$Package" + } + "ARM64" = @{ + "system" = "Darwin" + "machine" = "ARM64" + "file_name" = "$FileName-standalone-osx-arm64.zip" + "executable" = "$Package" + } + } + } + + if ($IsWindows) { + $os = "Windows" + # we only support x64 on windows, if that doesn't work the platform is unsupported + $machine = "AMD64" + } + elseif ($IsLinux) { + $os = "Linux" + $machine = Get-SystemArchitecture + } + elseif ($IsMacOS) { + $os = "Darwin" + $machine = Get-SystemArchitecture + } + else { + $os = "unknown" + } + + $ErrorActionPreference = $ErrorActionPreferenceDefault + + return $AVAILABLE_BINARIES[$os][$machine] +} + +function Clear-Directory ($path) { + if (Test-Path -Path $path) { + Remove-Item -Path $path -Recurse -Force + } + New-Item -ItemType Directory -Path $path -Force +} + +function isNewVersion( + [Parameter(mandatory = $true)] + $Version, + [Parameter(mandatory = $true)] + $Directory +) { + $savedVersionTxt = Join-Path $Directory "downloaded_version.txt" + if (Test-Path $savedVersionTxt) { + $result = (Get-Content -Raw $savedVersionTxt).Trim() + + if ($result -eq $Version) { + return $false + } + } + + return $true +} + +<# +.SYNOPSIS +Installs a standalone version of an engsys tool. +.PARAMETER Version +The version of the tool to install. Requires a full version to be provided. EG "1.0.0-dev.20240617.1" +.PARAMETER Directory +The directory within which the exe will exist after this function invokes. Defaults to "." +#> +function Install-Standalone-Tool ( + [Parameter()] + [string]$Version, + [Parameter(mandatory = $true)] + [string]$FileName, + [Parameter(mandatory = $true)] + [string]$Package, + [Parameter()] + [string]$Repository = "Azure/azure-sdk-tools", + [Parameter()] + $Directory = "." +) { + $ErrorActionPreference = "Stop" + $PSNativeCommandUseErrorActionPreference = $true + + $systemDetails = Get-Package-Meta -FileName $FileName -Package $Package + + if (!(Test-Path $Directory) -and $Directory -ne ".") { + New-Item -ItemType Directory -Path $Directory -Force | Out-Null + } + + $tag = "${Package}_${Version}" + + if (!$Version -or $Version -eq "*") { + Write-Host "Attempting to find latest version for package '$Package'" + $releasesUrl = "https://api.github.com/repos/$Repository/releases" + $releases = Invoke-RestMethod -Uri $releasesUrl + $found = $false + foreach ($release in $releases) { + if ($release.tag_name -like "$Package*") { + $tag = $release.tag_name + $Version = $release.tag_name -replace "${Package}_", "" + $found = $true + break + } + } + if ($found -eq $false) { + throw "No release found for package '$Package'" + } + } + + $downloadFolder = Resolve-Path $Directory + $downloadUrl = "https://github.com/$Repository/releases/download/$tag/$($systemDetails.file_name)" + $downloadFile = $downloadUrl.Split('/')[-1] + $downloadLocation = Join-Path $downloadFolder $downloadFile + $savedVersionTxt = Join-Path $downloadFolder "downloaded_version.txt" + $executable_path = Join-Path $downloadFolder $systemDetails.executable + + if (isNewVersion $version $downloadFolder) { + Write-Host "Installing '$Package' '$Version' to '$downloadFolder' from $downloadUrl" + Invoke-WebRequest -Uri $downloadUrl -OutFile $downloadLocation + + if ($downloadFile -like "*.zip") { + Expand-Archive -Path $downloadLocation -DestinationPath $downloadFolder -Force + } + elseif ($downloadFile -like "*.tar.gz") { + tar -xzf $downloadLocation -C $downloadFolder + } + else { + throw "Unsupported file format" + } + + # Remove the downloaded file after extraction + Remove-Item -Path $downloadLocation -Force + + # Record downloaded version + Set-Content -Path $savedVersionTxt -Value $Version + + # Set executable permissions if on macOS (Darwin) + if ($IsMacOS) { + chmod 755 $executable_path + } + } + else { + Write-Host "Target version '$Version' already present in target directory '$downloadFolder'" + } + + return $executable_path +} diff --git a/eng/common/testproxy/install-test-proxy.ps1 b/eng/common/testproxy/install-test-proxy.ps1 index 402e5ddc8..a97b45754 100644 --- a/eng/common/testproxy/install-test-proxy.ps1 +++ b/eng/common/testproxy/install-test-proxy.ps1 @@ -16,18 +16,15 @@ param( $InstallDirectory ) -. (Join-Path $PSScriptRoot test-proxy.ps1) +. (Join-Path $PSScriptRoot '..' 'scripts' 'Helpers' 'AzSdkTool-Helpers.ps1') Write-Host "Attempting to download and install version `"$Version`" into `"$InstallDirectory`"" -Install-Standalone-TestProxy -Version $Version -Directory $InstallDirectory +$exe = Install-Standalone-Tool ` + -Version $Version ` + -FileName "test-proxy" ` + -Package "Azure.Sdk.Tools.TestProxy" ` + -Directory $InstallDirectory -$PROXY_EXE = "" - -if ($IsWindows) { - $PROXY_EXE = Join-Path $InstallDirectory "Azure.Sdk.Tools.TestProxy.exe" -} else { - $PROXY_EXE = Join-Path $InstallDirectory "Azure.Sdk.Tools.TestProxy" -} -Write-Host "Downloaded test-proxy available at $PROXY_EXE." -Write-Host "##vso[task.setvariable variable=PROXY_EXE]$PROXY_EXE" +Write-Host "Downloaded test-proxy available at $exe." +Write-Host "##vso[task.setvariable variable=PROXY_EXE]$exe" diff --git a/eng/common/testproxy/test-proxy.ps1 b/eng/common/testproxy/test-proxy.ps1 deleted file mode 100644 index f1bf1eca8..000000000 --- a/eng/common/testproxy/test-proxy.ps1 +++ /dev/null @@ -1,162 +0,0 @@ -Set-StrictMode -Version 4 -$AVAILABLE_TEST_PROXY_BINARIES = @{ - "Windows" = @{ - "AMD64" = @{ - "system" = "Windows" - "machine" = "AMD64" - "file_name" = "test-proxy-standalone-win-x64.zip" - "executable" = "Azure.Sdk.Tools.TestProxy.exe" - } - } - "Linux" = @{ - "X86_64" = @{ - "system" = "Linux" - "machine" = "X86_64" - "file_name" = "test-proxy-standalone-linux-x64.tar.gz" - "executable" = "Azure.Sdk.Tools.TestProxy" - } - "ARM64" = @{ - "system" = "Linux" - "machine" = "ARM64" - "file_name" = "test-proxy-standalone-linux-arm64.tar.gz" - "executable" = "Azure.Sdk.Tools.TestProxy" - } - } - "Darwin" = @{ - "X86_64" = @{ - "system" = "Darwin" - "machine" = "X86_64" - "file_name" = "test-proxy-standalone-osx-x64.zip" - "executable" = "Azure.Sdk.Tools.TestProxy" - } - "ARM64" = @{ - "system" = "Darwin" - "machine" = "ARM64" - "file_name" = "test-proxy-standalone-osx-arm64.zip" - "executable" = "Azure.Sdk.Tools.TestProxy" - } - } -} - -function Get-SystemArchitecture { - $unameOutput = uname -m - switch ($unameOutput) { - "x86_64" { return "X86_64" } - "aarch64" { return "ARM64" } - "arm64" { return "ARM64" } - default { throw "Unable to determine system architecture. uname -m returned $unameOutput." } - } -} - -function Get-Proxy-Meta () { - $ErrorActionPreferenceDefault = $ErrorActionPreference - $ErrorActionPreference = "Stop" - - $os = "unknown" - $machine = Get-SystemArchitecture - - if ($IsWindows) { - $os = "Windows" - # we only support x64 on windows, if that doesn't work the platform is unsupported - $machine = "AMD64" - } elseif ($IsLinux) { - $os = "Linux" - } elseif ($IsMacOS) { - $os = "Darwin" - } - - $ErrorActionPreference = $ErrorActionPreferenceDefault - - return $AVAILABLE_TEST_PROXY_BINARIES[$os][$machine] -} - -function Get-Proxy-Url ( - [Parameter(mandatory=$true)]$Version -) { - $systemDetails = Get-Proxy-Meta - - $file = $systemDetails.file_name - $url = "https://github.com/Azure/azure-sdk-tools/releases/download/Azure.Sdk.Tools.TestProxy_$Version/$file" - - return $url -} - -function Cleanup-Directory ($path) { - if (Test-Path -Path $path) { - Remove-Item -Path $path -Recurse -Force - } - New-Item -ItemType Directory -Path $path -Force -} - -function Is-Work-Necessary ( - [Parameter(mandatory=$true)] - $Version, - [Parameter(mandatory=$true)] - $Directory -) { - $savedVersionTxt = Join-Path $Directory "downloaded_version.txt" - if (Test-Path $savedVersionTxt) { - $result = (Get-Content -Raw $savedVersionTxt).Trim() - - if ($result -eq $Version) { - return $false - } - } - - return $true -} - -<# -.SYNOPSIS -Installs a standalone version of the test-proxy. -.PARAMETER Version -The version of the proxy to install. Requires a full version to be provided. EG "1.0.0-dev.20240617.1" -.PARAMETER Directory -The directory within which the test-proxy exe will exist after this function invokes. Defaults to "." -#> -function Install-Standalone-TestProxy ( - [Parameter(mandatory=$true)] - $Version, - $Directory="." -) { - $ErrorActionPreference = "Stop" - $systemDetails = Get-Proxy-Meta - - if (!(Test-Path $Directory) -and $Directory -ne ".") { - New-Item -ItemType Directory -Path $Directory -Force - } - - $downloadFolder = Resolve-Path $Directory - $downloadUrl = Get-Proxy-Url $Version - $downloadFile = $downloadUrl.Split('/')[-1] - $downloadLocation = Join-Path $downloadFolder $downloadFile - $savedVersionTxt = Join-Path $downloadFolder "downloaded_version.txt" - - if (Is-Work-Necessary $version $downloadFolder) { - Write-Host "Commencing installation of `"$Version`" to `"$downloadFolder`" from $downloadUrl." - Invoke-WebRequest -Uri $downloadUrl -OutFile $downloadLocation - - if ($downloadFile -like "*.zip") { - Expand-Archive -Path $downloadLocation -DestinationPath $downloadFolder -Force - } elseif ($downloadFile -like "*.tar.gz") { - tar -xzf $downloadLocation -C $downloadFolder - } else { - throw "Unsupported file format" - } - - # Remove the downloaded file after extraction - Remove-Item -Path $downloadLocation -Force - - # Record downloaded version - Set-Content -Path $savedVersionTxt -Value $Version - - # Set executable permissions if on macOS (Darwin) - $executable_path = Join-Path $downloadFolder $systemDetails.executable - if ($IsMacOS) { - chmod 755 $executable_path - } - } - else { - Write-Host "Target version `"$Version`" already present in target directory `"$downloadFolder.`"" - } -}