Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .github/CODEOWNERS
Original file line number Diff line number Diff line change
Expand Up @@ -337,3 +337,5 @@
/src/storage-discovery/ @shanefujs @calvinhzy

/src/aks-agent/ @feiskyer @mainred @nilo19

/src/azext_mcp/ @ReaNAiveD
12 changes: 12 additions & 0 deletions src/mcp-server/HISTORY.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
.. :changelog:

Release History
===============

1.0.0b2
++++++
* Add support for what_if_preview_tool and best_practices_tool

1.0.0b1
++++++
* Initial release
5 changes: 5 additions & 0 deletions src/mcp-server/README.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
Microsoft Azure CLI 'mcp' Extension
==========================================

This package is for the 'mcp' extension.
i.e. 'az mcp'
28 changes: 28 additions & 0 deletions src/mcp-server/azext_mcp/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
# --------------------------------------------------------------------------------------------
# Copyright (c) Microsoft Corporation. All rights reserved.
# Licensed under the MIT License. See License.txt in the project root for license information.
# --------------------------------------------------------------------------------------------

from azure.cli.core import AzCommandsLoader

from azext_mcp._help import helps # pylint: disable=unused-import


class McpCommandsLoader(AzCommandsLoader):

def __init__(self, cli_ctx=None):
from azure.cli.core.commands import CliCommandType
mcp_custom = CliCommandType(operations_tmpl='azext_mcp.custom#{}')
super(McpCommandsLoader, self).__init__(cli_ctx=cli_ctx, custom_command_type=mcp_custom)

def load_command_table(self, args):
from azext_mcp.commands import load_command_table
load_command_table(self, args)
return self.command_table

def load_arguments(self, command):
from azext_mcp._params import load_arguments
load_arguments(self, command)


COMMAND_LOADER_CLS = McpCommandsLoader
18 changes: 18 additions & 0 deletions src/mcp-server/azext_mcp/_help.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# coding=utf-8
# --------------------------------------------------------------------------------------------
# Copyright (c) Microsoft Corporation. All rights reserved.
# Licensed under the MIT License. See License.txt in the project root for license information.
# --------------------------------------------------------------------------------------------

from knack.help_files import helps # pylint: disable=unused-import


helps['mcp'] = """
type: group
short-summary: CLI as local MCP servers.
"""

helps['mcp up'] = """
type: command
short-summary: local MCP server up.
"""
20 changes: 20 additions & 0 deletions src/mcp-server/azext_mcp/_params.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# --------------------------------------------------------------------------------------------
# Copyright (c) Microsoft Corporation. All rights reserved.
# Licensed under the MIT License. See License.txt in the project root for license information.
# --------------------------------------------------------------------------------------------
# pylint: disable=line-too-long

from knack.arguments import CLIArgumentType


def load_arguments(self, _):

with self.argument_context('mcp') as c:
pass

with self.argument_context('mcp up') as c:
# c.argument('port', required=False, default=8080, type=int, help='MCP server port.')
c.argument('disable_elicit', action='store_true',
help='Disable elicit confirmation for destructive commands. '
'Use with caution as it may lead to unintended actions.')
pass
143 changes: 143 additions & 0 deletions src/mcp-server/azext_mcp/azcli_script_insight_client.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
"""
Simple client example showing the methods for calling Azure Function App endpoints

IMPORTANT: The what-if service requires client-side authentication to operate under the
caller's subscription and permissions. Server-side authentication is not supported for
what-if operations as it would not provide access to the caller's subscription.

This client now uses DefaultAzureCredential which supports multiple authentication methods:
- Azure CLI: az login
- Environment variables (service principal): AZURE_CLIENT_ID, AZURE_CLIENT_SECRET, AZURE_TENANT_ID
- Managed Identity (when running in Azure environments)
- Visual Studio/VS Code authentication

The what-if service will use your configured credentials to access your subscription
and preview deployment changes under your permissions.
"""

import requests
import json
from typing import Dict, Any, Optional
from azure.identity import DefaultAzureCredential
from datetime import datetime, timezone
from azure.cli.core.util import send_raw_request


# Configuration
FUNCTION_APP_URL = "https://azcli-script-insight.azurewebsites.net"

def translate_cli_to_bicep(function_app_url: str, azcli_script: str) -> Dict[str, Any]:
"""
Translate Azure CLI script to Bicep template

Args:
function_app_url: Base URL of your Azure Function App
azcli_script: Azure CLI script to translate

Returns:
Dictionary with translation result
"""
url = f"{function_app_url.rstrip('/')}/api/cli_to_bicep"

headers = {
'Content-Type': 'application/json',
'Accept': 'application/json'
}

payload = {"azcli_script": azcli_script}

try:
response = requests.post(url, json=payload, headers=headers, timeout=300)
return response.json()
except requests.RequestException as e:
return {"error": str(e), "success": False}


def what_if_preview(cli_ctx, function_app_url: str, azcli_script: str, subscription_id: Optional[str] = None) -> Dict[str, Any]:
"""
Preview deployment changes using Azure what-if functionality

Args:
function_app_url: Base URL of your Azure Function App
azcli_script: Azure CLI script to analyze
subscription_id: Optional fallback subscription ID if not in script

Returns:
Dictionary with what-if preview result
"""
url = f"{function_app_url.rstrip('/')}/api/what_if_preview"

headers = {
'Content-Type': 'application/json',
'Accept': 'application/json'
}

payload = {"azcli_script": azcli_script}
if subscription_id:
payload["subscription_id"] = subscription_id

try:
response = send_raw_request(cli_ctx, "POST", url, body=json.dumps(payload), resource="https://management.azure.com")
return response.json()
except requests.RequestException as e:
return {"error": str(e), "success": False}


def analyze_azcli_script(function_app_url: str, azcli_script: str) -> Dict[str, Any]:
"""
Analyze Azure CLI script for best practices and recommendations

Args:
function_app_url: Base URL of your Azure Function App
azcli_script: Azure CLI script to analyze

Returns:
Dictionary with analysis result
"""
url = f"{function_app_url.rstrip('/')}/api/analyze_azcli_script"

headers = {
'Content-Type': 'application/json',
'Accept': 'application/json'
}

payload = {"azcli_script": azcli_script}

try:
response = requests.post(url, json=payload, headers=headers, timeout=30)
return response.json()
except requests.RequestException as e:
return {"error": str(e), "status": "error"}


# Example usage
if __name__ == "__main__":

# Sample Azure CLI script
sample_script = "# Create a resource group with uppercase name \n az group create --name azcli-script-insight --location eastus \n \n # Create a VM directly instead of using an ARM template \n az vm create --resource-group azcli-script-insight --name MyVM_01 --image UbuntuLTS --size Standard_D2s_v3 --admin-username azureuser --generate-ssh-keys \n \n # Create a VMSS without auto-scaling \n az vmss create --resource-group azcli-script-insight --name MyVMSS --image UbuntuLTS --instance-count 3 --admin-username azureuser --generate-ssh-keys \n # Create a web app without managed identity \n az webapp create --resource-group azcli-script-insight --plan MyAppServicePlan --name MyWebApp \n \n # Create duplicate resource group (redundant) \n az group create --name azcli-script-insight --location eastus \n \n # Loop through VMs and query details individually (inefficient) \n for vm in $(az vm list --resource-group azcli-script-insight --query \"[].name\" -o tsv); do \n az vm show --resource-group azcli-script-insight --name $vm \n done"
# 1. Translate CLI to Bicep
print("=== CLI to Bicep Translation ===")
translation_result = translate_cli_to_bicep(FUNCTION_APP_URL, sample_script)
if translation_result.get("success"):
print("Translation successful!")
print(f"Bicep Template:\n{translation_result['bicep_template']}")
else:
print(f"Translation failed: {translation_result.get('error')}")

# 2. What-If Preview (requires client-side Azure CLI credentials)
print("\n=== What-If Preview (Client-side Azure CLI Auth) ===")
whatif_cli_result = what_if_preview(FUNCTION_APP_URL, sample_script, subscription_id = '6b085460-5f21-477e-ba44-1035046e9101')
if whatif_cli_result.get("success"):
print("What-if preview with CLI auth successful!")
print(f"Changes: {json.dumps(whatif_cli_result['what_if_result'], indent=2)}")
else:
print(f"{whatif_cli_result}")

# 3. Analyze Script
print("\n=== Script Analysis ===")
analysis_result = analyze_azcli_script(FUNCTION_APP_URL, sample_script)
if analysis_result.get("status") == "success":
print("Analysis successful!")
print(f"Analysis: {json.dumps(analysis_result['analysis'], indent=2)}")
else:
print(f"Analysis failed: {analysis_result.get('error')}")
4 changes: 4 additions & 0 deletions src/mcp-server/azext_mcp/azext_metadata.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"azext.isPreview": true,
"azext.minCliCoreVersion": "2.57.0"
}
Loading
Loading