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

  1. Prerequisites
  2. Step 1: Create an Azure Service Principal
  3. Step 2: Install Required Software
  4. Step 3: Set Up the MCP Server
  5. Step 4: Configure Claude Desktop
  6. Step 5: Test Your Setup
  7. Available Commands
  8. 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

  1. Go to https://portal.azure.com
  2. Sign in with your Azure account

1.2 Register a New Application

  1. In the search bar, type "App registrations" and select it
  2. Click "+ New registration"
  3. Fill in the details:
    • Name: azure-vnet-mcp-server (or any name you prefer)
    • Supported account types: Select "Accounts in this organizational directory only"
  4. 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

  1. In the left menu, click "Certificates & secrets"
  2. Click "+ New client secret"
  3. Add a description: MCP Server Secret
  4. Choose expiration: 12 months (recommended)
  5. Click "Add"
  6. ⚠️ 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

  1. In the search bar, type "Subscriptions" and select it
  2. Click on your subscription
  3. 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:

  1. Go to your Subscription (search "Subscriptions" in the portal)
  2. Click "Access control (IAM)" in the left menu
  3. Click "+ Add""Add role assignment"
  4. Select role: "Network Contributor"
  5. Click "Next"
  6. Click "+ Select members"
  7. Search for your app name (azure-vnet-mcp-server)
  8. Select it and click "Select"
  9. 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

  1. Find the Claude icon in your system tray (bottom-right of your screen)
  2. Right-click on the Claude icon
  3. Click "Quit" or "Exit"
  4. 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:

  1. Check if Python is installed: python --version
  2. Check if packages are installed: pip list | Select-String "azure|mcp"
  3. Test the script manually: python "C:\mcp\azure-vnet-dns\azure_vnet_dns_mcp.py"
  4. Check Claude logs: Click "Open Logs Folder" in Claude settings

Error: "Authentication failed"

Solutions:

  1. Verify your credentials are correct in the Python script
  2. Check that your Service Principal has the right permissions
  3. 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:

  1. Make sure claude_desktop_config.json is valid JSON (no syntax errors)
  2. Verify the Python script path is correct
  3. 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

  1. Never commit credentials to Git - Add the Python script to .gitignore
  2. Use short-lived secrets - Set client secrets to expire in 6-12 months
  3. Principle of least privilege - Only grant Network Contributor, not Owner
  4. Rotate secrets regularly - Create new secrets before old ones expire
  5. Monitor usage - Check Azure Activity Log for unusual activity

Next Steps

Now that you have the basic setup working, you can:

  1. Add more Azure services - Extend the script to manage VMs, Storage, etc.
  2. Add error handling - Improve error messages for better troubleshooting
  3. Add confirmation prompts - Ask before destructive operations
  4. 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

Popular posts from this blog

Bridging the Impossible: Connecting Jira On-Prem to Power Automate & Copilot Studio — The Solution Nobody Built Until Now"

How I Automated My Entire SharePoint Tenant with 150 MCP Tools and Claude Desktop

Azure Management MCP Server