Blog Post

Microsoft Mission Critical Blog
13 MIN READ

MCP Servers Explained — Part 1: Understand and Build Your First One

BabulalGhule21's avatar
Sep 24, 2026

A friendly, no-jargon walk-through. Part 1 of a two-part series: understand what an MCP server is and build a real one in Python — a VM inventory tool for Azure — then test it on your own machine. Part 2 takes it to the cloud, connects it to Copilot Studio, and secures it. By Babulal Ghule — Cloud Solution Architect, Microsoft

If you have been hearing the term “MCP server” everywhere and quietly nodding along without fully getting it — this post is for you. My goal is simple: by the time you reach the bottom of this page, you will understand what an MCP server is, why it matters, and you will have built one yourself and tested it on your laptop. This is Part 1 of a two-part series — Part 2 covers deploying to Azure, connecting Copilot Studio, and securing it. No prior AI background needed. Let’s learn it together.

1. A quick story: the problem before MCP

Think back to the days when every phone, camera, and gadget came with its own charger. A drawer full of cables, and none of them worked with anything else. Then USB-C arrived — one shape, one plug, everything just connects. Life got simpler. 

AI assistants had the exact same messy-drawer problem. On their own, a large language model (the “brain” behind tools like Copilot) is brilliant at conversation but completely cut off from your real world. It cannot see today’s cloud spend, tell you which VMs you’re running, or check your calendar unless someone writes custom “glue code” for each and every connection. 

Here is the painful part. If you had 3 AI apps that each needed to talk to 3 systems (say a database, your email, and a CRM), you had to build and maintain 3 × 3 = 9 separate integrations. Add one more app or one more system, and the number explodes. Every team reinvents the same wheel. 

Figure 1 — MCP turns a tangle of one-off integrations into a single, shared standard.

2. So, what exactly is an MCP server?

MCP stands for Model Context Protocol. It is an open standard — a shared set of rules — that lets AI assistants connect to your data, tools, and systems in one consistent way. It was introduced by Anthropic in late 2024 and has since been adopted widely across the industry, including by Microsoft. 

The easiest way to picture it: 

In plain words — MCP is like a USB-C port for AI. Instead of a different cable for every device, there is one standard socket. An MCP server is simply a small program that plugs one of your systems (a database, an API, a file share) into that socket, so any AI assistant that speaks MCP can use it. 

So an MCP server is a lightweight service you write. It advertises a menu of things the AI is allowed to do — for example “list every VM SKU in a region” or “look up an order by number” — and it does the actual work when the AI asks. The AI never touches your systems directly; it just politely asks your server, and your server stays fully in control. 

The best analogy is a waiter in a restaurant. You (the AI) do not walk into the kitchen. You tell the waiter (the MCP server) what you want from the menu. The waiter goes to the kitchen (your database or API) and brings back exactly what was ordered — nothing more. Clean, safe, predictable. 

3. The building blocks (in plain words)

There are only a handful of pieces to learn. Once these click, everything else makes sense. 

Piece 

What it means — in everyday language 

Host 

The AI app you actually use — Copilot Studio, VS Code with GitHub Copilot, Claude Desktop, and so on. It holds the “brain” (the language model). 

Client 

A small connector that lives inside the host. It speaks MCP and keeps a one-to-one line open to a server. You rarely build this yourself — the host provides it. 

Server 

The part you build. It exposes your data and actions to the AI in a safe, structured way. 

Tools 

Actions the AI can perform — “list your VMs”, “send an email”, “query sales”. Think of these as verbs. This is what you will use most. 

Resources 

Read-only information the AI can pull in for context — a document, a config file, a record. Think of these as nouns. 

Prompts 

Ready-made instruction templates your server can offer, so users get consistent, high-quality results. 

Transport 

The “wire” the messages travel over. stdio for a server running on your own machine; streamable HTTP for a server running remotely in the cloud (what we use here). 

Figure 2 — The host holds the model and one client per server; each server safely exposes one system.

4. How a request actually flows

Let’s make it concrete. Imagine you ask your AI assistant: “How many D-series VMs are we running in East US, and in which zones?” Here is what happens behind the scenes, in plain steps. 

 

Figure 3 — The round trip of a single question, from you to your data and back.
  • You ask in plain language. “How many D-series VMs are we running in East US, and in which zones?” The agent reads your intent. 
  • The agent picks a tool. It looks at the menu of tools your server published, and decides get_vm_inventory is the right one. 
  • The client calls your server. Through the MCP client, it sends a structured request to your MCP server — safely, over the agreed protocol. 
  • Your server does the real work. It runs one Azure Resource Graph query, counts the SKUs by region and zone, and returns a clean result. 
  • The agent answers you. It turns the raw result into a friendly sentence: “In East US you run 10 D4s_v5 VMs — 6 in zone 1 and 4 in zone 2.” 

5. Why should you care? (the “aha” moment)

An AI that only chats is a clever intern with no access to the building. The moment you plug in an MCP server, that intern gets a security badge and can actually get things done. Here is what changes: 

  • Write once, use everywhere. Build a server for your system a single time, and every MCP-aware assistant — today and tomorrow — can use it. No rewrites when you switch tools. 
  • Your data stays yours. The AI never gets a copy of your systems. It asks; your server decides what to return. You control every boundary. 
  • Live answers, not stale guesses. Because the server talks to real systems, the AI works with today’s numbers, not last year’s training data. 
  • Small pieces, big power. Each server does one job well. Combine several and an agent can check your VM inventory, look up a cost report, and email the team — in one flow. 
  • It sparks innovation. Once connecting a system takes an afternoon instead of a month, people start automating things they never bothered to before. 

The big picture — “Unplugged,” an AI agent is smart but stuck. MCP is what plugs it in. That is the whole reason this topic is worth your time — it is the difference between an assistant that talks and one that acts.

6. Let’s build one: your first MCP server in Python

Enough theory — let’s build something genuinely useful: a VM Inventory server. Give it a list of Azure subscriptions and it reports every virtual machine and scale-set SKU you run, grouped by region and availability zone. It uses the official MCP Python SDK and Azure Resource Graph to read your estate. The whole server is just two small files — server.py (the tool) and vmcp.py (the Azure logic) — and the very same code runs on your laptop and later in the cloud. 

What you need first — Python 3.11 or 3.12 (avoid 3.13/3.14, which don’t have compatible wheels yet), the Azure CLI, and an Azure subscription you can read (the tool needs the Reader role). On your own machine, a quick az login is all the sign-in it needs to start. 

 

Step 1 — Set up the project 

📂 Source Code 
All code samples, scripts, and configuration files used in this article are available in the GitHub repository below. Feel free to clone the repository, explore the code, and adapt it for your own environment. 

GitHub: [GitHub Repository Link]

mkdir vm-inventory-mcp && cd vm-inventory-mcp python -m venv .venv source .venv/bin/activate # on Windows: .venv\Scripts\activate

Install everything with one command: 

pip install -r requirements.txt

 

Step 2 — Write the server and its tool 

This is where the magic lives. server.py creates the MCP server, sets up how it signs in to Azure, and exposes a single, focused tool — get_vm_inventory. Each function marked with MCp.tool() becomes an action the AI can call, and the AI reads its docstring to decide when to use it, so write it clearly. Notice the credential logic: in the cloud the server uses a managed identity (no secrets), and locally it uses your az login — the same code, no change. 

server.py 

 

"""MCP server — Azure VM Capacity Planner: VM Inventory sample. A focused starter kit extracted from the full capacity-planning MCP server. It exposes a single tool, get_vm_inventory, which reports region/zone-wise VM & VMSS SKU inventory across subscriptions using Azure Resource Graph. - Transport: streamable HTTP (so Copilot Studio / VS Code / Agent Framework can reach it). - Auth to Azure: * In Azure Container Apps -> the container's MANAGED IDENTITY (no secrets in code). System-assigned works out of the box; for a user-assigned identity also set the env var AZURE_CLIENT_ID to that identity's client id. * Locally -> your `az login` (DefaultAzureCredential). - Access needed: Reader on the target subscription(s), granted to that identity. - Endpoint: http://localhost:8000/mcp """ from __future__ import annotations import os from typing import List from mcp.server.fastmcp import FastMCP from azure.identity import DefaultAzureCredential, ManagedIdentityCredential import vmcp # host/port so it binds correctly inside the container (honors the Dockerfile ENV). mcp = FastMCP( "vm-inventory-mcp", host=os.environ.get("HOST", "0.0.0.0"), port=int(os.environ.get("PORT", "8000")), ) def _build_credential(): """Choose the right Azure credential for wherever this runs. In Azure Container Apps a managed identity is injected (IDENTITY_ENDPOINT is set), so we use ManagedIdentityCredential DIRECTLY -- no secrets, no keys. - SYSTEM-assigned identity: nothing else to set. - USER-assigned identity: also set the env var AZURE_CLIENT_ID to that identity's client id, so we bind to the correct one. Locally (no managed identity) we fall back to your `az login` via DefaultAzureCredential. """ client_id = os.environ.get("AZURE_CLIENT_ID") # set only for a user-assigned identity if os.environ.get("IDENTITY_ENDPOINT"): if client_id: return ManagedIdentityCredential(client_id=client_id) return ManagedIdentityCredential() return DefaultAzureCredential() # Managed identity in the cloud, az login locally. _cred = _build_credential() MCp.tool() def get_vm_inventory(subscriptions: List[str]) -> dict: """Region/zone-wise VM & VMSS SKU inventory across subscriptions (Azure Resource Graph). Needs azure-mgmt-resourcegraph + Reader on the subscriptions. (VMCP 'inventory').""" return vmcp.inventory_vm_skus(_cred, subscriptions) if __name__ == "__main__": # Remote hosting: expose over HTTP so Copilot Studio / Agent Framework can reach it. # Your MCP endpoint will be http://localhost:8000/mcp (https://<app-url>/mcp in the cloud) mcp.run(transport="streamable-http")

Notice how short the tool is. That is on purpose — the tool layer just names the action and hands off to a helper. All the Azure work lives in a separate vmcp.py file, which runs one Azure Resource Graph query and folds the results into a tidy region → zone → SKU count. Keeping them apart means the AI-facing part stays easy to read, and the Azure logic is easy to test on its own. 

vmcp.py 

 

"""VMCP inventory helper Contains the Azure Resource Graph logic behind the get_vm_inventory tool: inventory_vm_skus plus its small helpers, kept faithful to the original file. """ from __future__ import annotations import datetime as _dt from collections import defaultdict from typing import Any, Dict, Sequence from azure.identity import DefaultAzureCredential try: from azure.mgmt.resourcegraph import ResourceGraphClient from azure.mgmt.resourcegraph.models import QueryRequest _ARG_IMPORT_ERROR: Exception | None = None except ImportError as exc: # keep the real reason so the tool can report it ResourceGraphClient = None # type: ignore QueryRequest = None # type: ignore _ARG_IMPORT_ERROR = exc def _now_iso() -> str: return _dt.datetime.utcnow().replace(microsecond=0).isoformat() + "Z" def normalize_region(region: str) -> str: return (region or "").strip().lower().replace(" ", "") def inventory_vm_skus(credential: DefaultAzureCredential, subscriptions: Sequence[str]) -> Dict[str, Any]: if _ARG_IMPORT_ERROR is not None: raise RuntimeError( "Could not import azure-mgmt-resourcegraph. Install it in the SAME environment " "that runs the server ('pip install azure-mgmt-resourcegraph'), then RESTART the " f"server. Original import error: {_ARG_IMPORT_ERROR!r}" ) rg_client = ResourceGraphClient(credential) query = """ resources | where type =~ 'microsoft.compute/virtualmachines' or type =~ 'microsoft.compute/virtualmachinescalesets' | extend vmSize = case( type =~ 'microsoft.compute/virtualmachines', tostring(properties.hardwareProfile.vmSize), type =~ 'microsoft.compute/virtualmachinescalesets', tostring(sku.name), '' ) | project subscriptionId, resourceGroup, name, type, location, zones, vmSize """ req = QueryRequest(subscriptions=list(subscriptions), query=query, options={"resultFormat": "objectArray"}) res = rg_client.resources(req) rows = res.data or [] norm_rows = [] for r in rows: loc = normalize_region(r.get("location")) zones = r.get("zones") if isinstance(zones, list): zlist = [str(z) for z in zones] elif zones is None: zlist = [] else: zlist = [z.strip() for z in str(zones).strip("[]").replace("'", "").split(",") if z.strip()] norm_rows.append( { "subscriptionId": r.get("subscriptionId"), "resourceGroup": r.get("resourceGroup"), "name": r.get("name"), "type": r.get("type"), "location": loc, "zones": zlist, "vmSize": r.get("vmSize"), } ) agg = defaultdict(lambda: defaultdict(lambda: defaultdict(int))) for r in norm_rows: reg = r["location"] or "unknown" sku = r["vmSize"] or "unknown" if r["zones"]: for z in r["zones"]: agg[reg][str(z)][sku] += 1 else: agg[reg]["regional"][sku] += 1 return { "generatedAt": _now_iso(), "subscriptions": list(subscriptions), "items": norm_rows, "aggregates": {reg: {zone: dict(skus) for zone, skus in zones.items()} for reg, zones in agg.items()}, }

 

Reading the tool like the AI does — The AI never sees the Azure code; it only sees the tool’s name (get_vm_inventory), its one input (subscriptions), and its docstring. That is exactly why the docstring spells out what it returns and what access it needs. Clear names in, clear results out. 

One more file rounds out the project: a Dockerfile (so we can ship it to the cloud in Part 2), and a .vscode/mcp.json that points VS Code at the server. You can create these now, but you won’t need them until you test with Copilot or deploy.

7. Test it on your own machine

This server runs with the MCP SDK’s own HTTP server — there is no uvicorn or separate app file. Sign in to Azure first (so it has an identity to use), then start it:

az login python server.py

Leave it running. Your MCP server is now live at http://localhost:8000/mcp. Everything below happens while it stays up. 

A 30-second sanity check (a tiny test script) 

Before wiring up a full AI assistant, it’s smart to prove the server responds and see the tool it offers. The mcp package you already installed includes a small client too, so we can write a short script that connects, lists the tools, and calls one. No extra installs needed. 

test_local.py (core) 

Point it at your subscriptions — In a second terminal (leave the server running), tell the test which subscriptions to read, then run it. macOS/Linux: export AZURE_SUBSCRIPTION_IDS="<your-sub-id>". Windows PowerShell: $env:AZURE_SUBSCRIPTION_IDS="<your-sub-id>". Then: python test_local.py. You only need the read-only Reader role. 

If everything is wired up, you’ll see the tool listed and a live inventory printed — something like this:

expected output 

Server exposes 1 tool(s): - get_vm_inventory { "generatedAt": "2026-08-02T06:10:04Z", "aggregates": { "westus2": {"1": {"Standard_D2ads_v6": 1}, "2": {"Standard_D2ads_v6": 1}}, "centralindia": {"1": {"Standard_B2als_v2": 2}}, "eastus2": {"regional": {"Standard_D4s_v3": 1}} } }

Test the error path too — The included test also calls the tool with a bogus subscription id. Azure rejects it, the MCP client sees isError=True, and the test prints a clean one-line PASS instead of a crash. Good tools always answer — even when the answer is “that didn’t work.” It’s the perfect spot to add Pydantic validation later. 

Other quick ways to see the tool 

  • MCP Inspector (visual, no code): run npx @modelcontextprotocol/inspector, choose the HTTP transport, enter http://localhost:8000/mcp, and click Connect. You get a friendly screen that lists the tool, shows its inputs, and lets you run it by hand. 
  • VS Code Tools list: add the server (below), open the Tools panel in Copilot Chat, and get_vm_inventory appears with a checkbox. 

Ask a real assistant: GitHub Copilot in Agent mode 

The kit ships a .vscode/mcp.json that points VS Code at http://localhost:8000/mcp. Open Copilot Chat, switch to Agent mode, start the server from that file (or via the Command Palette → MCP: List Servers), confirm get_vm_inventory in the Tools list, then ask: “Show me our VM inventory by region and zone.” Approve the tool call and you’ll see your live estate summarised — built in a couple of dozen lines of code. 

.vscode/mcp.json { "servers": { "vm-inventory-mcp": { "type": "http", "url": "http://localhost:8000/mcp" } } }

Wrapping up Part 1 — and what’s next 

That’s the hard part done. You now understand what an MCP server is, why it matters, and you’ve built a real one in Python — a VM inventory tool that reads your Azure estate and reports it by region and zone — and tested it on your own machine, both with a script and with GitHub Copilot.

Coming up in Part 2 — We take this exact server to the cloud on Azure Container Apps, give it a read-only managed identity (no secrets in the code), connect it to a Copilot Studio agent, secure it with Microsoft Entra ID, and walk through the common problems people hit. Keep learning, keep sharing — and I’ll see you in Part 2.

References 

  • Model Context Protocol — official documentation: https://modelcontextprotocol.io/ 
  • MCP Python SDK (GitHub): https://github.com/modelcontextprotocol/python-sdk 
  • Azure Resource Graph documentation: https://learn.microsoft.com/en-us/azure/governance/resource-graph/ 

About the author: Babulal Ghule is a Cloud Solution Architect at Microsoft. He enjoys turning complex cloud and AI topics into simple, hands-on guides that anyone can follow. 

Updated Sep 24, 2026
Version 1.0
No CommentsBe the first to comment