Skip to content

Commit 958f8a4

Browse files
author
Xing Zhou
committed
Register Script Insight MCP tools
1 parent a2dc8d7 commit 958f8a4

21 files changed

Lines changed: 1962 additions & 0 deletions

.github/CODEOWNERS

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -337,3 +337,5 @@
337337
/src/storage-discovery/ @shanefujs @calvinhzy
338338

339339
/src/aks-agent/ @feiskyer @mainred @nilo19
340+
341+
/src/azext_mcp/ @ReaNAiveD

src/mcp-server/HISTORY.rst

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
.. :changelog:
2+
3+
Release History
4+
===============
5+
6+
1.0.0b2
7+
++++++
8+
* Add support for what_if_preview_tool and best_practices_tool
9+
10+
1.0.0b1
11+
++++++
12+
* Initial release

src/mcp-server/README.rst

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
Microsoft Azure CLI 'mcp' Extension
2+
==========================================
3+
4+
This package is for the 'mcp' extension.
5+
i.e. 'az mcp'
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
# --------------------------------------------------------------------------------------------
2+
# Copyright (c) Microsoft Corporation. All rights reserved.
3+
# Licensed under the MIT License. See License.txt in the project root for license information.
4+
# --------------------------------------------------------------------------------------------
5+
6+
from azure.cli.core import AzCommandsLoader
7+
8+
from azext_mcp._help import helps # pylint: disable=unused-import
9+
10+
11+
class McpCommandsLoader(AzCommandsLoader):
12+
13+
def __init__(self, cli_ctx=None):
14+
from azure.cli.core.commands import CliCommandType
15+
mcp_custom = CliCommandType(operations_tmpl='azext_mcp.custom#{}')
16+
super(McpCommandsLoader, self).__init__(cli_ctx=cli_ctx, custom_command_type=mcp_custom)
17+
18+
def load_command_table(self, args):
19+
from azext_mcp.commands import load_command_table
20+
load_command_table(self, args)
21+
return self.command_table
22+
23+
def load_arguments(self, command):
24+
from azext_mcp._params import load_arguments
25+
load_arguments(self, command)
26+
27+
28+
COMMAND_LOADER_CLS = McpCommandsLoader

src/mcp-server/azext_mcp/_help.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
# coding=utf-8
2+
# --------------------------------------------------------------------------------------------
3+
# Copyright (c) Microsoft Corporation. All rights reserved.
4+
# Licensed under the MIT License. See License.txt in the project root for license information.
5+
# --------------------------------------------------------------------------------------------
6+
7+
from knack.help_files import helps # pylint: disable=unused-import
8+
9+
10+
helps['mcp'] = """
11+
type: group
12+
short-summary: CLI as local MCP servers.
13+
"""
14+
15+
helps['mcp up'] = """
16+
type: command
17+
short-summary: local MCP server up.
18+
"""
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
# --------------------------------------------------------------------------------------------
2+
# Copyright (c) Microsoft Corporation. All rights reserved.
3+
# Licensed under the MIT License. See License.txt in the project root for license information.
4+
# --------------------------------------------------------------------------------------------
5+
# pylint: disable=line-too-long
6+
7+
from knack.arguments import CLIArgumentType
8+
9+
10+
def load_arguments(self, _):
11+
12+
with self.argument_context('mcp') as c:
13+
pass
14+
15+
with self.argument_context('mcp up') as c:
16+
# c.argument('port', required=False, default=8080, type=int, help='MCP server port.')
17+
c.argument('disable_elicit', action='store_true',
18+
help='Disable elicit confirmation for destructive commands. '
19+
'Use with caution as it may lead to unintended actions.')
20+
pass
Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
1+
"""
2+
Simple client example showing the methods for calling Azure Function App endpoints
3+
4+
IMPORTANT: The what-if service requires client-side authentication to operate under the
5+
caller's subscription and permissions. Server-side authentication is not supported for
6+
what-if operations as it would not provide access to the caller's subscription.
7+
8+
This client now uses DefaultAzureCredential which supports multiple authentication methods:
9+
- Azure CLI: az login
10+
- Environment variables (service principal): AZURE_CLIENT_ID, AZURE_CLIENT_SECRET, AZURE_TENANT_ID
11+
- Managed Identity (when running in Azure environments)
12+
- Visual Studio/VS Code authentication
13+
14+
The what-if service will use your configured credentials to access your subscription
15+
and preview deployment changes under your permissions.
16+
"""
17+
18+
import requests
19+
import json
20+
from typing import Dict, Any, Optional
21+
from azure.identity import DefaultAzureCredential
22+
from datetime import datetime, timezone
23+
from azure.cli.core.util import send_raw_request
24+
25+
26+
# Configuration
27+
FUNCTION_APP_URL = "https://azcli-script-insight.azurewebsites.net"
28+
29+
def translate_cli_to_bicep(function_app_url: str, azcli_script: str) -> Dict[str, Any]:
30+
"""
31+
Translate Azure CLI script to Bicep template
32+
33+
Args:
34+
function_app_url: Base URL of your Azure Function App
35+
azcli_script: Azure CLI script to translate
36+
37+
Returns:
38+
Dictionary with translation result
39+
"""
40+
url = f"{function_app_url.rstrip('/')}/api/cli_to_bicep"
41+
42+
headers = {
43+
'Content-Type': 'application/json',
44+
'Accept': 'application/json'
45+
}
46+
47+
payload = {"azcli_script": azcli_script}
48+
49+
try:
50+
response = requests.post(url, json=payload, headers=headers, timeout=300)
51+
return response.json()
52+
except requests.RequestException as e:
53+
return {"error": str(e), "success": False}
54+
55+
56+
def what_if_preview(cli_ctx, function_app_url: str, azcli_script: str, subscription_id: Optional[str] = None) -> Dict[str, Any]:
57+
"""
58+
Preview deployment changes using Azure what-if functionality
59+
60+
Args:
61+
function_app_url: Base URL of your Azure Function App
62+
azcli_script: Azure CLI script to analyze
63+
subscription_id: Optional fallback subscription ID if not in script
64+
65+
Returns:
66+
Dictionary with what-if preview result
67+
"""
68+
url = f"{function_app_url.rstrip('/')}/api/what_if_preview"
69+
70+
headers = {
71+
'Content-Type': 'application/json',
72+
'Accept': 'application/json'
73+
}
74+
75+
payload = {"azcli_script": azcli_script}
76+
if subscription_id:
77+
payload["subscription_id"] = subscription_id
78+
79+
try:
80+
response = send_raw_request(cli_ctx, "POST", url, body=json.dumps(payload), resource="https://management.azure.com")
81+
return response.json()
82+
except requests.RequestException as e:
83+
return {"error": str(e), "success": False}
84+
85+
86+
def analyze_azcli_script(function_app_url: str, azcli_script: str) -> Dict[str, Any]:
87+
"""
88+
Analyze Azure CLI script for best practices and recommendations
89+
90+
Args:
91+
function_app_url: Base URL of your Azure Function App
92+
azcli_script: Azure CLI script to analyze
93+
94+
Returns:
95+
Dictionary with analysis result
96+
"""
97+
url = f"{function_app_url.rstrip('/')}/api/analyze_azcli_script"
98+
99+
headers = {
100+
'Content-Type': 'application/json',
101+
'Accept': 'application/json'
102+
}
103+
104+
payload = {"azcli_script": azcli_script}
105+
106+
try:
107+
response = requests.post(url, json=payload, headers=headers, timeout=30)
108+
return response.json()
109+
except requests.RequestException as e:
110+
return {"error": str(e), "status": "error"}
111+
112+
113+
# Example usage
114+
if __name__ == "__main__":
115+
116+
# Sample Azure CLI script
117+
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"
118+
# 1. Translate CLI to Bicep
119+
print("=== CLI to Bicep Translation ===")
120+
translation_result = translate_cli_to_bicep(FUNCTION_APP_URL, sample_script)
121+
if translation_result.get("success"):
122+
print("Translation successful!")
123+
print(f"Bicep Template:\n{translation_result['bicep_template']}")
124+
else:
125+
print(f"Translation failed: {translation_result.get('error')}")
126+
127+
# 2. What-If Preview (requires client-side Azure CLI credentials)
128+
print("\n=== What-If Preview (Client-side Azure CLI Auth) ===")
129+
whatif_cli_result = what_if_preview(FUNCTION_APP_URL, sample_script, subscription_id = '6b085460-5f21-477e-ba44-1035046e9101')
130+
if whatif_cli_result.get("success"):
131+
print("What-if preview with CLI auth successful!")
132+
print(f"Changes: {json.dumps(whatif_cli_result['what_if_result'], indent=2)}")
133+
else:
134+
print(f"{whatif_cli_result}")
135+
136+
# 3. Analyze Script
137+
print("\n=== Script Analysis ===")
138+
analysis_result = analyze_azcli_script(FUNCTION_APP_URL, sample_script)
139+
if analysis_result.get("status") == "success":
140+
print("Analysis successful!")
141+
print(f"Analysis: {json.dumps(analysis_result['analysis'], indent=2)}")
142+
else:
143+
print(f"Analysis failed: {analysis_result.get('error')}")
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
{
2+
"azext.isPreview": true,
3+
"azext.minCliCoreVersion": "2.57.0"
4+
}

0 commit comments

Comments
 (0)