The earlier article in this series introduced the Model Context Protocol by Anthropic. It showed an example of building a simple MCP server that you can use with the Claude desktop application. The hello-world example was a very basic implementation of an MCP server. In today’s article, we shall extend that knowledge of creating MCP servers to do something more useful. We shall build an MCP server for interacting with Microsoft Azure resources.
Anthropic made bootstrap MCP server development easy by providing the create-mcp-server package. To get started, you need to install this as a tool locally.
1
2
3
4
5
6
# Using uvx (recommended)
uvx create-mcp-server
# Or using pip
pip install create-mcp-server
create-mcp-server
To create a new MCP server, run the uvx create-mcp-server command and follow the prompts on the screen.
PS C:\GitHub> uvx create-mcp-server
Creating a new MCP server project using uv.
This will set up a Python project with MCP dependency.
Let's begin!
Project name (required): azure-mcp-server
Project description [A MCP server project]: An MCP server to interact with Azure resources
Project version [0.1.0]:
Project will be created at: C:\GitHub\azure-mcp-server
Is this correct? [Y/n]: Y
Using CPython 3.13.1 interpreter at: C:\Program Files\Python313\python.exe
Creating virtual environment at: .venv
Resolved 21 packages in 466ms
Built azure-mcp-server @ file:///C:/GitHub/azure-mcp-server
Prepared 2 packages in 2.02s
Installed 21 packages in 207ms
+ annotated-types==0.7.0
+ anyio==4.8.0
+ azure-mcp-server==0.1.0 (from file:///C:/GitHub/azure-mcp-server)
+ certifi==2025.1.31
+ click==8.1.8
+ colorama==0.4.6
+ h11==0.14.0
+ httpcore==1.0.7
+ httpx==0.28.1
+ httpx-sse==0.4.0
+ idna==3.10
+ mcp==1.3.0
+ pydantic==2.10.6
+ pydantic-core==2.27.2
+ pydantic-settings==2.8.1
+ python-dotenv==1.0.1
+ sniffio==1.3.1
+ sse-starlette==2.2.1
+ starlette==0.46.0
+ typing-extensions==4.12.2
+ uvicorn==0.34.0
Claude.app detected. Would you like to install the server into Claude.app now? [Y/n]: Y
Settings file location: C:\Users\ravik\AppData\Roaming\Claude\claude_desktop_config.json
✅ Created project azure-mcp-server in azure-mcp-server
ℹ️ To install dependencies run:
cd azure-mcp-server
uv sync --dev --all-extras
This command sets up all the dependencies needed to build an MCP server. Depending on your choice, it will also add the MCP server to the Claude Desktop configuration. The folder structure will be as follows.
The src\azure_mcp_server should contain all the business logic you need to enable Azure resource management integration. By default, it contains a sample MCP Server used to manage notes.
This sample server implementation is a great start to learning how to implement different capabilities of an MCP server. We shall implement the tools’ capability and, in the future, look at implementing prompts and resources as well.
You must decide how to authenticate to interact with the Azure resource management API. For the purpose of the demonstration, I have used client secret-based authentication. I documented these requirements in an earlier article. The client secret credential is better created by adding the keys and secrets as environment variables. We can use the .env file in the Python project to make this easy. You need to add the following key-value pairs to this file.
With all the package dependencies added to the project, we can move toward adding the necessary tools. This is done in server.py. Before adding the code related to the tools, let us first add the functions needed to talk to the Azure resource management API.
asyncdeflist_subscriptions()->list[dict[str,Any]]:"""List all subscriptions in the account.
Args:
None
"""credential=EnvironmentCredential()subscription_client=SubscriptionClient(credential)subscriptions=subscription_client.subscriptions.list()subscription_list=[]forsubscriptioninlist(subscriptions):subscription_info={"id":subscription.subscription_id,"name":subscription.display_name,}subscription_list.append(subscription_info)returnsubscription_listasyncdeflist_resource_groups(subscription_id=None)->list[dict[str,Any]]:"""List all resource groups in the subscription.
Args:
subscription_id (str): The subscription ID. This is an optional parameter.
"""credential=EnvironmentCredential()ifsubscription_idisNone:if"AZURE_SUBSCRIPTION_ID"notinos.environ:raiseValueError("subscription_id must be provided or set as an environment variable.")else:subscription_id=os.environ["AZURE_SUBSCRIPTION_ID"]resource_client=ResourceManagementClient(credential,subscription_id)group_list=resource_client.resource_groups.list()resource_groups=[]forgroupinlist(group_list):resource={"name":group.name,"location":group.location,}resource_groups.append(resource)returnresource_groupsasyncdeflist_resources(resource_group,subscription_id)->list[dict[str,Any]]:"""List all resources in the resource group.
Args:
resource_group (str): The resource group name.
subscription_id (str): The subscription ID. This is an optional parameter.
"""credential=EnvironmentCredential()ifsubscription_idisNone:if"AZURE_SUBSCRIPTION_ID"notinos.environ:raiseValueError("subscription_id must be provided or set as an environment variable.")else:subscription_id=os.environ["AZURE_SUBSCRIPTION_ID"]resource_client=ResourceManagementClient(credential,subscription_id)resources=resource_client.resources.list_by_resource_group(resource_group)resource_list=[]forresourceinlist(resources):resource_info={"name":resource.name,"type":resource.type,"location":resource.location,}resource_list.append(resource_info)returnresource_list
These three functions are a basic implementation for getting a list of subscriptions, all resource groups in a subscription, and all resources within a resource group. You must have the docstring inside each function to describe what the function is about and its arguments, and outputs. The code within these functions is self-explanatory. If you need a quick tour of Azure resource management in Python, look at the Azure Python SDK.
An MCP server is a JSON RPC server. Every MCP server exposes the list and call tool endpoints. These are defined using the handle_list_tools() and handle_call_tool() functions.
@server.list_tools()asyncdefhandle_list_tools()->list[types.Tool]:"""
List available tools.
Each tool specifies its arguments using JSON Schema validation.
"""return[types.Tool(name="list-subscriptions",description="List all Azure subscriptions for the authenticated user.",inputSchema={"type":"object","properties":{},"required":[],},),types.Tool(name="list-resource-groups",description="List all resource groups in an Azure subscription.",inputSchema={"type":"object","properties":{"subscription_id":{"type":"string"},},"required":[],},),types.Tool(name="list-resources",description="List all resources in a resource group.",inputSchema={"type":"object","properties":{"subscription_id":{"type":"string"},"resource_group":{"type":"string"}},"required":["resource_group"],},)]@server.call_tool()asyncdefhandle_call_tool(name:str,arguments:dict|None)->list[types.TextContent|types.ImageContent|types.EmbeddedResource]:"""
Handle tool execution requests.
Tools can modify server state and notify clients of changes.
"""ifname=="list-subscriptions":response=awaitlist_subscriptions()respText="Subscriptions:\n"forsubscriptioninresponse:respText+=f"ID: {subscription['id']}, Name: {subscription['name']}\n"elifname=="list-resource-groups":subscription_id=arguments.get("subscription_id",None)response=awaitlist_resource_groups(subscription_id)respText=f"Resource Groups in {subscription_id}:\n"forgroupinresponse:respText+=f"Name: {group['name']}, Location: {group['location']}\n"elifname=="list-resources":subscription_id=arguments.get("subscription_id",None)resource_group=arguments.get("resource_group")result=awaitlist_resources(resource_group,subscription_id)respText=f"Resources in {resource_group} in the {subscription_id}:\n"forresourceinresult:respText+=f"Name: {resource['name']}, Type: {resource['type']}, Location: {resource['location']}\n"else:respText="Invalid tool name."return[types.TextContent(type="text",text=respText)]
These list and call functions are decorated using the list_tools() and call_tools() decorators respectively. The handle_list_tools() returns a list of tools where each element is of type types.Tool. The handle_call_tool() returns the output from the tool call as one of the return types specified in the function signature. Depending on the return type, you must construct the value. In this example, all tools call will respond with a dictionary. This response then gets converted to text content and is returned as types.TextContent type. This type requires type and text properties.
As the create-mcp-server command added the tool to the Claude Desktop application, you must be able to see the tools ready for use.
Once you confirm the available tools, you can try the following prompts.
List all subscriptions I have access to in my Azure account
Do I have any resource groups in the east-us region?
List all virtual machines provisioned in my Research subscription.
When you prompt, Claude will ask permission to use the available tools. If allowed, it can call the tools and get you the response.
With MCP, the possibilities are endless. I am developing the Azure MCP server as an open-source project, and I will continue to add more tools, prompts, and resources to it. Do check it out and leave a comment.