How to Connect Claude Desktop to Azure: Build Your Own VNET & DNS MCP Server
How to Connect Claude Desktop to Azure: Build Your Own VNET & DNS MCP Server
A Complete Beginner's Guide to Managing Azure Virtual Networks with Claude AI
Introduction
Imagine asking Claude to "create a new virtual network in Azure" and watching it happen automatically. This guide shows you how to build a Model Context Protocol (MCP) server that connects Claude Desktop directly to your Azure subscription, allowing you to manage Virtual Networks (VNets) and DNS zones using natural language.
By the end of this tutorial, you'll be able to:
- List, create, and delete Azure Virtual Networks
- Manage subnets within your VNets
- Create and manage DNS zones and records
- All through simple conversations with Claude!
Table of Contents
- Prerequisites
- Step 1: Create an Azure Service Principal
- Step 2: Install Required Software
- Step 3: Set Up the MCP Server
- Step 4: Configure Claude Desktop
- Step 5: Test Your Setup
- Available Commands
- Troubleshooting
Prerequisites
Before you begin, make sure you have:
- ✅ Windows 10/11 computer
- ✅ Claude Desktop installed (Download here)
- ✅ Python 3.10+ installed (Download here)
- ✅ Azure subscription with Owner or Contributor access
- ✅ Azure CLI installed (Download here)
Verify Python Installation
Open PowerShell and run:
python --version
You should see something like Python 3.11.x or higher.
Step 1: Create an Azure Service Principal
A Service Principal is like a special account that allows applications to access your Azure resources securely.
1.1 Sign in to Azure Portal
- Go to https://portal.azure.com
- Sign in with your Azure account
1.2 Register a New Application
- In the search bar, type "App registrations" and select it
- Click "+ New registration"
- Fill in the details:
- Name:
azure-vnet-mcp-server(or any name you prefer) - Supported account types: Select "Accounts in this organizational directory only"
- Name:
- Click "Register"
1.3 Copy Your Application Credentials
After registration, you'll see the Overview page. Copy these values and save them somewhere safe:
| Field | Where to Find It | Your Value |
|---|---|---|
| Application (client) ID | Overview page | xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx |
| Directory (tenant) ID | Overview page | xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx |
1.4 Create a Client Secret
- In the left menu, click "Certificates & secrets"
- Click "+ New client secret"
- Add a description:
MCP Server Secret - Choose expiration: 12 months (recommended)
- Click "Add"
- ⚠️ IMPORTANT: Copy the Value immediately! You won't be able to see it again.
| Field | Your Value |
|---|---|
| Client Secret | your-secret-value-here |
1.5 Get Your Subscription ID
- In the search bar, type "Subscriptions" and select it
- Click on your subscription
- Copy the Subscription ID
| Field | Your Value |
|---|---|
| Subscription ID | xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx |
1.6 Assign Permissions to Your App
Your Service Principal needs permission to manage network resources:
- Go to your Subscription (search "Subscriptions" in the portal)
- Click "Access control (IAM)" in the left menu
- Click "+ Add" → "Add role assignment"
- Select role: "Network Contributor"
- Click "Next"
- Click "+ Select members"
- Search for your app name (
azure-vnet-mcp-server) - Select it and click "Select"
- Click "Review + assign" twice
Optional: If you want to manage DNS zones, repeat steps 3-9 and add the "DNS Zone Contributor" role.
Step 2: Install Required Software
2.1 Install Python Packages
Open PowerShell as Administrator and run:
pip install azure-identity azure-mgmt-network azure-mgmt-dns azure-mgmt-resource mcp
Wait for all packages to install. You should see "Successfully installed..." messages.
2.2 Verify Installation
pip list | Select-String "azure|mcp"
You should see output like:
azure-identity 1.15.0
azure-mgmt-dns 8.1.0
azure-mgmt-network 25.0.0
azure-mgmt-resource 23.0.0
mcp 0.9.0
Step 3: Set Up the MCP Server
3.1 Create the Project Folder
New-Item -ItemType Directory -Force -Path "C:\mcp\azure-vnet-dns"
3.2 Create the MCP Server Script
Open Notepad or VS Code and create a new file with the following content:
#!/usr/bin/env python3
"""
Azure VNET and DNS MCP Server
Connect Claude Desktop to Azure for network management
"""
import sys
import json
# ============================================
# 🔐 AZURE CREDENTIALS - ENTER YOUR VALUES HERE
# ============================================
TENANT_ID = "YOUR_TENANT_ID_HERE" # Example: "e6c360d6-57a5-473e-9188-3bfac0e3250e"
CLIENT_ID = "YOUR_CLIENT_ID_HERE" # Example: "bf1ba8f4-0a8b-4dbf-af4e-8407693299b6"
CLIENT_SECRET = "YOUR_CLIENT_SECRET_HERE" # Example: "Xkq8Q~PQtWCx7cwQKQmmbhH112..."
SUBSCRIPTION_ID = "YOUR_SUBSCRIPTION_ID_HERE" # Example: "a9307114-91f9-4e69-ab19-3fec0b52ef70"
# ============================================
# Validate credentials are filled in
if "YOUR_" in TENANT_ID or "YOUR_" in CLIENT_ID or "YOUR_" in CLIENT_SECRET or "YOUR_" in SUBSCRIPTION_ID:
print("ERROR: Please edit the script and replace the placeholder values with your Azure credentials!", file=sys.stderr)
print("Look for the section marked 'AZURE CREDENTIALS' at the top of the script.", file=sys.stderr)
sys.exit(1)
try:
from azure.identity import ClientSecretCredential
from azure.mgmt.network import NetworkManagementClient
from azure.mgmt.dns import DnsManagementClient
from azure.mgmt.resource import ResourceManagementClient
except ImportError:
print("Missing Azure SDK. Run: pip install azure-identity azure-mgmt-network azure-mgmt-dns azure-mgmt-resource", file=sys.stderr)
sys.exit(1)
try:
from mcp.server.fastmcp import FastMCP
except ImportError:
print("Missing MCP SDK. Run: pip install mcp", file=sys.stderr)
sys.exit(1)
# Initialize Azure connection
credential = ClientSecretCredential(
tenant_id=TENANT_ID,
client_id=CLIENT_ID,
client_secret=CLIENT_SECRET
)
network_client = NetworkManagementClient(credential, SUBSCRIPTION_ID)
dns_client = DnsManagementClient(credential, SUBSCRIPTION_ID)
resource_client = ResourceManagementClient(credential, SUBSCRIPTION_ID)
# Initialize MCP Server
mcp = FastMCP("azure-vnet-dns")
# ============================================
# AUTHENTICATION & INFO TOOLS
# ============================================
@mcp.tool()
def whoami_subscription() -> str:
"""Check Azure authentication status and show subscription info."""
try:
sub = resource_client.subscriptions.get(SUBSCRIPTION_ID)
return json.dumps({
"status": "authenticated",
"subscription_id": SUBSCRIPTION_ID,
"subscription_name": sub.display_name,
"state": str(sub.state)
}, indent=2)
except Exception as e:
return json.dumps({"error": str(e)})
@mcp.tool()
def list_resource_groups() -> str:
"""List all resource groups in the subscription."""
try:
groups = []
for rg in resource_client.resource_groups.list():
groups.append({
"name": rg.name,
"location": rg.location
})
return json.dumps({"resource_groups": groups, "count": len(groups)}, indent=2)
except Exception as e:
return json.dumps({"error": str(e)})
# ============================================
# VIRTUAL NETWORK TOOLS
# ============================================
@mcp.tool()
def list_vnets(resource_group: str = "") -> str:
"""List all Virtual Networks. Optionally filter by resource group name."""
try:
vnets = []
if resource_group:
vnet_list = network_client.virtual_networks.list(resource_group)
else:
vnet_list = network_client.virtual_networks.list_all()
for vnet in vnet_list:
vnets.append({
"name": vnet.name,
"location": vnet.location,
"resource_group": vnet.id.split('/')[4] if vnet.id else "",
"address_space": vnet.address_space.address_prefixes if vnet.address_space else [],
"subnets": [s.name for s in vnet.subnets] if vnet.subnets else []
})
return json.dumps({"vnets": vnets, "count": len(vnets)}, indent=2)
except Exception as e:
return json.dumps({"error": str(e)})
@mcp.tool()
def get_vnet(resource_group: str, vnet_name: str) -> str:
"""Get detailed information about a specific Virtual Network."""
try:
vnet = network_client.virtual_networks.get(resource_group, vnet_name)
subnets = []
if vnet.subnets:
for subnet in vnet.subnets:
subnets.append({"name": subnet.name, "address_prefix": subnet.address_prefix})
return json.dumps({
"name": vnet.name,
"location": vnet.location,
"address_space": vnet.address_space.address_prefixes if vnet.address_space else [],
"subnets": subnets
}, indent=2)
except Exception as e:
return json.dumps({"error": str(e)})
@mcp.tool()
def create_vnet(resource_group: str, vnet_name: str, location: str, address_prefix: str) -> str:
"""
Create a new Virtual Network.
Parameters:
- resource_group: Name of the resource group (e.g., 'myResourceGroup')
- vnet_name: Name for the new VNet (e.g., 'myVNet')
- location: Azure region (e.g., 'eastus', 'westus2', 'westeurope')
- address_prefix: CIDR notation (e.g., '10.0.0.0/16')
Example: create_vnet('myRG', 'myVnet', 'eastus', '10.0.0.0/16')
"""
try:
poller = network_client.virtual_networks.begin_create_or_update(
resource_group, vnet_name,
{"location": location, "address_space": {"address_prefixes": [address_prefix]}}
)
vnet = poller.result()
return json.dumps({"status": "success", "message": f"VNet '{vnet_name}' created successfully"}, indent=2)
except Exception as e:
return json.dumps({"error": str(e)})
@mcp.tool()
def delete_vnet(resource_group: str, vnet_name: str) -> str:
"""Delete a Virtual Network. WARNING: This action cannot be undone!"""
try:
poller = network_client.virtual_networks.begin_delete(resource_group, vnet_name)
poller.result()
return json.dumps({"status": "success", "message": f"VNet '{vnet_name}' deleted successfully"})
except Exception as e:
return json.dumps({"error": str(e)})
# ============================================
# SUBNET TOOLS
# ============================================
@mcp.tool()
def list_subnets(resource_group: str, vnet_name: str) -> str:
"""List all subnets in a Virtual Network."""
try:
subnets = []
for subnet in network_client.subnets.list(resource_group, vnet_name):
subnets.append({"name": subnet.name, "address_prefix": subnet.address_prefix})
return json.dumps({"subnets": subnets, "count": len(subnets)}, indent=2)
except Exception as e:
return json.dumps({"error": str(e)})
@mcp.tool()
def create_subnet(resource_group: str, vnet_name: str, subnet_name: str, address_prefix: str) -> str:
"""
Create a new subnet in a Virtual Network.
Parameters:
- resource_group: Name of the resource group
- vnet_name: Name of the existing VNet
- subnet_name: Name for the new subnet (e.g., 'web-subnet')
- address_prefix: CIDR notation within VNet range (e.g., '10.0.1.0/24')
Example: create_subnet('myRG', 'myVnet', 'web-subnet', '10.0.1.0/24')
"""
try:
poller = network_client.subnets.begin_create_or_update(
resource_group, vnet_name, subnet_name, {"address_prefix": address_prefix}
)
poller.result()
return json.dumps({"status": "success", "message": f"Subnet '{subnet_name}' created successfully"}, indent=2)
except Exception as e:
return json.dumps({"error": str(e)})
@mcp.tool()
def delete_subnet(resource_group: str, vnet_name: str, subnet_name: str) -> str:
"""Delete a subnet from a Virtual Network."""
try:
poller = network_client.subnets.begin_delete(resource_group, vnet_name, subnet_name)
poller.result()
return json.dumps({"status": "success", "message": f"Subnet '{subnet_name}' deleted successfully"})
except Exception as e:
return json.dumps({"error": str(e)})
# ============================================
# DNS ZONE TOOLS
# ============================================
@mcp.tool()
def list_dns_zones(resource_group: str = "") -> str:
"""List all DNS zones. Optionally filter by resource group."""
try:
zones = []
if resource_group:
zone_list = dns_client.zones.list_by_resource_group(resource_group)
else:
zone_list = dns_client.zones.list()
for zone in zone_list:
zones.append({
"name": zone.name,
"resource_group": zone.id.split('/')[4] if zone.id else "",
"name_servers": zone.name_servers
})
return json.dumps({"dns_zones": zones, "count": len(zones)}, indent=2)
except Exception as e:
return json.dumps({"error": str(e)})
@mcp.tool()
def create_dns_zone(resource_group: str, zone_name: str) -> str:
"""
Create a new DNS zone.
Parameters:
- resource_group: Name of the resource group
- zone_name: Domain name for the zone (e.g., 'example.com', 'myapp.io')
Example: create_dns_zone('myRG', 'example.com')
"""
try:
zone = dns_client.zones.create_or_update(resource_group, zone_name, {"location": "global"})
return json.dumps({
"status": "success",
"message": f"DNS zone '{zone_name}' created successfully",
"name_servers": zone.name_servers
}, indent=2)
except Exception as e:
return json.dumps({"error": str(e)})
@mcp.tool()
def delete_dns_zone(resource_group: str, zone_name: str) -> str:
"""Delete a DNS zone. WARNING: This will delete all DNS records in the zone!"""
try:
poller = dns_client.zones.begin_delete(resource_group, zone_name)
poller.result()
return json.dumps({"status": "success", "message": f"DNS zone '{zone_name}' deleted successfully"})
except Exception as e:
return json.dumps({"error": str(e)})
# ============================================
# DNS RECORD TOOLS
# ============================================
@mcp.tool()
def list_dns_records(resource_group: str, zone_name: str) -> str:
"""List all DNS records in a zone."""
try:
records = []
for record in dns_client.record_sets.list_by_dns_zone(resource_group, zone_name):
record_type = record.type.split('/')[-1] if record.type else ""
values = []
if record.a_records:
values = [r.ipv4_address for r in record.a_records]
elif record.cname_record:
values = [record.cname_record.cname]
elif record.txt_records:
values = [' '.join(r.value) for r in record.txt_records]
elif record.ns_records:
values = [r.nsdname for r in record.ns_records]
elif record.mx_records:
values = [f"{r.preference} {r.exchange}" for r in record.mx_records]
records.append({"name": record.name, "type": record_type, "ttl": record.ttl, "values": values})
return json.dumps({"records": records, "count": len(records)}, indent=2)
except Exception as e:
return json.dumps({"error": str(e)})
@mcp.tool()
def create_dns_record(resource_group: str, zone_name: str, record_name: str, record_type: str, value: str, ttl: int = 3600) -> str:
"""
Create a DNS record in a zone.
Parameters:
- resource_group: Name of the resource group
- zone_name: Domain name of the zone (e.g., 'example.com')
- record_name: Name of the record (e.g., 'www', '@' for root, 'mail')
- record_type: Type of record (A, CNAME, TXT, MX)
- value: Record value (IP for A, hostname for CNAME, text for TXT)
- ttl: Time to live in seconds (default: 3600)
Examples:
- A record: create_dns_record('myRG', 'example.com', 'www', 'A', '1.2.3.4')
- CNAME: create_dns_record('myRG', 'example.com', 'blog', 'CNAME', 'myblog.wordpress.com')
- TXT: create_dns_record('myRG', 'example.com', '@', 'TXT', 'v=spf1 include:_spf.google.com ~all')
"""
try:
record_type = record_type.upper()
params = {"ttl": ttl}
if record_type == "A":
params["a_records"] = [{"ipv4_address": value}]
elif record_type == "AAAA":
params["aaaa_records"] = [{"ipv6_address": value}]
elif record_type == "CNAME":
params["cname_record"] = {"cname": value}
elif record_type == "TXT":
params["txt_records"] = [{"value": [value]}]
elif record_type == "MX":
parts = value.split()
pref = int(parts[0]) if len(parts) > 1 else 10
exchange = parts[-1]
params["mx_records"] = [{"preference": pref, "exchange": exchange}]
else:
return json.dumps({"error": f"Unsupported record type: {record_type}. Use A, AAAA, CNAME, TXT, or MX."})
dns_client.record_sets.create_or_update(resource_group, zone_name, record_name, record_type, params)
return json.dumps({"status": "success", "message": f"DNS record '{record_name}' ({record_type}) created successfully"}, indent=2)
except Exception as e:
return json.dumps({"error": str(e)})
@mcp.tool()
def delete_dns_record(resource_group: str, zone_name: str, record_name: str, record_type: str) -> str:
"""Delete a DNS record from a zone."""
try:
dns_client.record_sets.delete(resource_group, zone_name, record_name, record_type.upper())
return json.dumps({"status": "success", "message": f"DNS record '{record_name}' deleted successfully"})
except Exception as e:
return json.dumps({"error": str(e)})
# ============================================
# START THE SERVER
# ============================================
if __name__ == "__main__":
mcp.run()
3.3 Save the File
Save this file as:
C:\mcp\azure-vnet-dns\azure_vnet_dns_mcp.py
3.4 Edit Your Credentials
Open the file you just saved and find this section near the top:
# ============================================
# 🔐 AZURE CREDENTIALS - ENTER YOUR VALUES HERE
# ============================================
TENANT_ID = "YOUR_TENANT_ID_HERE"
CLIENT_ID = "YOUR_CLIENT_ID_HERE"
CLIENT_SECRET = "YOUR_CLIENT_SECRET_HERE"
SUBSCRIPTION_ID = "YOUR_SUBSCRIPTION_ID_HERE"
# ============================================
Replace each placeholder with your actual values from Step 1:
| Placeholder | Replace With |
|---|---|
YOUR_TENANT_ID_HERE |
Your Directory (tenant) ID from Azure |
YOUR_CLIENT_ID_HERE |
Your Application (client) ID from Azure |
YOUR_CLIENT_SECRET_HERE |
Your Client Secret value |
YOUR_SUBSCRIPTION_ID_HERE |
Your Azure Subscription ID |
Example (with fake values):
TENANT_ID = "e6c360d6-57a5-473e-9188-3bfac0e3250e"
CLIENT_ID = "bf1ba8f4-0a8b-4dbf-af4e-8407693299b6"
CLIENT_SECRET = "Xkq8Q~PQtWCx7cwQKQmmbhH112aCqBobpK4Yecah"
SUBSCRIPTION_ID = "a9307114-91f9-4e69-ab19-3fec0b52ef70"
⚠️ Security Note: Never share this file or commit it to Git with your real credentials!
Step 4: Configure Claude Desktop
4.1 Open Claude Desktop Config
In PowerShell, run:
notepad "$env:APPDATA\Claude\claude_desktop_config.json"
If the file doesn't exist, Notepad will ask if you want to create it. Click Yes.
4.2 Add the MCP Server Configuration
Replace the entire file content with:
{
"mcpServers": {
"azure-vnet-dns": {
"command": "python",
"args": ["C:\\mcp\\azure-vnet-dns\\azure_vnet_dns_mcp.py"]
}
}
}
Save and close Notepad.
4.3 Restart Claude Desktop
- Find the Claude icon in your system tray (bottom-right of your screen)
- Right-click on the Claude icon
- Click "Quit" or "Exit"
- Reopen Claude Desktop from your Start menu
Step 5: Test Your Setup
5.1 Manual Test (Recommended First)
Before testing in Claude, verify the script works by running it manually in PowerShell:
python "C:\mcp\azure-vnet-dns\azure_vnet_dns_mcp.py"
If successful, you'll see the server start. Press Ctrl+C to stop it.
Common errors:
- "Please edit the script and replace the placeholder values" → You forgot to add your credentials
- "Missing Azure SDK" → Run the pip install command from Step 2
- "Authentication failed" → Check your credentials are correct
5.2 Test in Claude Desktop
Open Claude Desktop and try these commands:
Check authentication:
Check my Azure subscription status
List VNets:
Show me all my Azure virtual networks
List resource groups:
List all my Azure resource groups
If Claude responds with your Azure data, congratulations! 🎉 Your setup is complete!
Available Commands
Here's what you can ask Claude to do:
Authentication & Info
| Command | Description |
|---|---|
| Check my Azure subscription | Verify authentication and show subscription info |
| List my resource groups | Show all resource groups |
Virtual Networks
| Command | Description |
|---|---|
| List all my VNets | Show all virtual networks |
| Show VNet details for [name] in [resource-group] | Get detailed VNet info |
| Create a VNet named [name] in [location] with address space [CIDR] | Create new VNet |
| Delete VNet [name] | Remove a VNet |
Subnets
| Command | Description |
|---|---|
| List subnets in VNet [name] | Show all subnets |
| Create subnet [name] with address [CIDR] in VNet [vnet-name] | Create new subnet |
| Delete subnet [name] from VNet [vnet-name] | Remove a subnet |
DNS Zones
| Command | Description |
|---|---|
| List my DNS zones | Show all DNS zones |
| Create DNS zone for [domain.com] | Create new DNS zone |
| Delete DNS zone [domain.com] | Remove a DNS zone |
DNS Records
| Command | Description |
|---|---|
| List DNS records in [domain.com] | Show all records in a zone |
| Create A record [name] pointing to [IP] in [domain.com] | Create A record |
| Create CNAME [name] pointing to [target] in [domain.com] | Create CNAME record |
| Delete [record-name] record from [domain.com] | Remove a record |
Troubleshooting
Error: "MCP azure-vnet-dns: Server disconnected"
Solutions:
- Check if Python is installed:
python --version - Check if packages are installed:
pip list | Select-String "azure|mcp" - Test the script manually:
python "C:\mcp\azure-vnet-dns\azure_vnet_dns_mcp.py" - Check Claude logs: Click "Open Logs Folder" in Claude settings
Error: "Authentication failed"
Solutions:
- Verify your credentials are correct in the Python script
- Check that your Service Principal has the right permissions
- Ensure your client secret hasn't expired
Error: "Missing Azure SDK" or "Missing MCP SDK"
Solution:
pip install azure-identity azure-mgmt-network azure-mgmt-dns azure-mgmt-resource mcp
MCP Server Not Showing in Claude
Solutions:
- Make sure
claude_desktop_config.jsonis valid JSON (no syntax errors) - Verify the Python script path is correct
- Completely quit and restart Claude Desktop (not just close the window)
View Claude Desktop Logs
Get-Content "$env:APPDATA\Claude\logs\mcp*.log" -Tail 100
Security Best Practices
- Never commit credentials to Git - Add the Python script to
.gitignore - Use short-lived secrets - Set client secrets to expire in 6-12 months
- Principle of least privilege - Only grant Network Contributor, not Owner
- Rotate secrets regularly - Create new secrets before old ones expire
- Monitor usage - Check Azure Activity Log for unusual activity
Next Steps
Now that you have the basic setup working, you can:
- Add more Azure services - Extend the script to manage VMs, Storage, etc.
- Add error handling - Improve error messages for better troubleshooting
- Add confirmation prompts - Ask before destructive operations
- Create multiple environments - Set up separate Service Principals for dev/prod
Conclusion
You've successfully connected Claude Desktop to Azure! You can now manage your Azure Virtual Networks and DNS zones using natural language. This is just the beginning – the same pattern can be used to connect Claude to any API or service.
Questions or issues? Feel free to reach out or open an issue on the project repository.
Happy automating! 🚀
Last updated: December 2024

Comments
Post a Comment