Drive carbon reductions in cloud migrations with Sustainability insights in Azure Migrate
June 18, 2025Mastering Model Context Protocol (MCP): Building Multi Server MCP with Azure OpenAI
June 18, 2025So why Microsoft Learn? Well, it’s a treasure trove of knowledge for developers and IT pros. Secondly, because it has search page with many filters, it lends itself well to be wrapped as a MCP server.
I’m talking about this page of course Microsoft Learn.
MCP?
Let’s back up the tape a little, MCP, what’s that? MCP stands for Model Context Protocol and is a new standard for dealing with AI apps. The idea is that you have features like prompts, tools and resources that you can use to build AI applications. Because it’s a standard, you can easily share these features with others. In fact, you can use MCP servers running locally as well as ones that runs on an IP Address. Pait that with and Agentic client like Visual Studio Code or Claude Desktop and you have built an Agent.
How do we build this?
Well, the idea is to “wrap” Microsoft Learn’s API for search for training content. This means that we will create an MCP server with tools that we can call. So first off, what parts of the API do we need?
- Free text search. We definitely want this one as it allows us to search for training content with keywords.
- Filters. We want to know what filters exist so we can search for content based on them.
- Topic, there are many different topics like products, roles and more, let’s support this too.
Building the MCP Server
For this, we will use a transport method called SSE, Server-Sent Events. This is a simple way to create a server that people can interact with using HTTP. Here’s how the server code will look:
from mcp.server.fastmcp import FastMCP
from starlette.applications import Starlette
from starlette.routing import Mount, Host
mcp = FastMCP(“Microsoft Learn Search”, “0.1.0”)
@mcp.tool()
def learn_filter() -> list[Filter]:
pass
@mcp.tool()
def free_text(query: str) -> list[Result]:
pass
@mcp.tool()
def topic_search(category: str, topic: str) -> list[Result]:
pass
port = 8000
app = Starlette(
routes=[
Mount(‘/’, app=mcp.sse_app()),
]
)
This code sets up a basic MCP server using FastMCP and Starlette. The learn_filter, free_text, and topic_search functions are placeholders for the actual implementation of the tools that will interact with the Microsoft Learn API. Note the use of the decorator “@mcp.tool” which registers these functions as tools in the MCP server. Look also at the result types Filter and Result, which are used to define the structure of the data returned by these tools.
Implementing the Tools
We will implement the tools one by one.
Implementing the learn_filter Tool
So let’s start with the learn_filter function. This function will retrieve the available filters from the Microsoft Learn API. Here’s how we can implement it:
MCP.tool()
def learn_filter() -> list[Filter]:
data = search_learn()
return data
Next, let’s implement search_learn like so:
# search/search.py
import requests
from config import BASE_URL
from utils.models import Filter
def search_learn() -> list[Filter]:
“””
Top level search function to fetch learn data from the Microsoft Learn API.
“””
url = to_url()
response = requests.get(url)
if response.status_code == 200:
data = response.json()
facets = data[“facets”]
filters = []
# print(facets.keys())
for key in facets.keys():
# print(f”{key}”)
for item in facets[key]:
filter = Filter(**item)
filters.append(filter)
# print(f” – {filter.value} ({filter.count})”)
return filters
else:
return None
def to_url():
return f”{BASE_URL}/api/contentbrowser/search?environment=prod&locale=en-us&facet=roles&facet=levels&facet=products&facet=subjects&facet=resource_type&%24top=30&showHidden=false&fuzzySearch=false”
# config.py
BASE_URL = “https://learn.microsoft.com”
We also need to define the Filter model, we can use Pydantic for this:
# utils/models.py
class Filter(BaseModel):
type: str
value: str
count: int
How do we know what this looks like, well, we’ve tried to make a request to the API and looked at the response, here’s an excerpt of the response:
{
“facets”: {
“roles”: [
{
“type”: “role”,
“value”: “Administrator”,
“count”: 10
},
{
“type”: “role”,
“value”: “Developer”,
“count”: 20
}
],
…
}
}
Technically, each item has more properties, but we only need the type, value, and count for our purposes.
Implementing the free_text Tool
For this, we will implement the free_text function, like so:
# search/free_text.py
import requests
from utils.models import Result
from config import BASE_URL
def search_free_text(text: str) -> list[Result]:
url = to_url(text)
response = requests.get(url)
results = []
if response.status_code == 200:
data = response.json()
if “results” in data:
records = len(data[“results”])
print(f”Search results: {records} records found”)
for item in data[“results”]:
result = Result(**item)
result.url = f”{BASE_URL}{result.url}”
results.append(result)
return results
def to_url(text: str):
return f”{BASE_URL}/api/contentbrowser/search?environment=prod&locale=en-us&terms={text}&facet=roles&facet=levels&facet=products&facet=subjects&facet=resource_type&%24top=30&showHidden=false&fuzzySearch=false”
Here we use a new type Result, which we also need to define. This will represent the search results returned by the Microsoft Learn API. Let’s define it:
# utils/models.py
from pydantic import BaseModel
class Result(BaseModel):
title: str
url: str
popularity: float
summary: str | None = None
Finally, let’s wire this up in our MCP server:
MCP.tool()
def learn_filter() -> list[Filter]:
data = search_learn()
return data
@mcp.tool()
def free_text(query: str) -> list[Result]:
print(“LOG: free_text called with query:”, query)
data = search_free_text(query)
return data
Implementing Search by topic
Just one more method to implement, the search_topic method, let’s implement it like so:
import requests
from utils.models import Result
from config import BASE_URL
def search_topic(topic: str, category: str =”products”) -> list[Result]:
results = []
url = to_url(category, topic)
response = requests.get(url)
if response.status_code == 200:
data = response.json()
for item in data[“results”]:
result = Result(**item)
result.url = f”{BASE_URL}{result.url}”
results.append(result)
return results
else:
return []
def to_url(category: str, topic: str):
return f”{BASE_URL}/api/contentbrowser/search?environment=prod&locale=en-us&facet=roles&facet=levels&facet=products&facet=subjects&facet=resource_type&%24filter=({category}%2Fany(p%3A%20p%20eq%20%27{topic}%27))&%24top=30&showHidden=false&fuzzySearch=false”
Great, this one also uses Result as the return type, so we only have left to wire this up in our MCP server:
MCP.tool()
def topic_search(category: str, topic: str) -> list[Result]:
print(“LOG: topic_search called with category:”, category, “and topic:”, topic)
data = search_topic(topic, category)
return data
That’s it, let’s move on to testing it.
Testing the MCP Server with Visual Studio Code
You can test this with Visual Studio Code and its Agent mode. What you need to do is:
Create a file mcp.json in the .vscode directory and make sure it looks like this:
{
“inputs”: [],
“servers”: {
“learn”: {
“type”: “sse”,
“url”: “http://localhost:8000/sse”
}
}
}
Note how we point to a running server on port 8000, this is where our MCP server will run.
Installing Dependencies
We will need the following dependencies to run this code:
pip install requests “mcp[cli]”
You’re also recommended to create a virtual environment for this project, you can do this with:
python -m venv venv source venv/bin/activate # On Windows use `venvScriptsactivate`
Then install the dependencies as shown above.
Running the MCP Server
To run the MCP server, you can use the following command:
uvicorn server:app
How this one works is Uvicorn looks for a file server.py and an app variable in that file, which is the Starlette app we created earlier.
Try it out
We got the server running, and an entry in Visual Studio Code. Click the play icon just above your entry in Visual Studio Code, this should connect to the MCP server.
Try it out by typing in the search box in the bottom right of your GitHub Copilot extension. For example, type “Do a free text search on JavaScript, use a tool”. It should look something like this:
You should see the response coming back from Microsoft Learn and your MCP Server.
Summary
Congrats, if you’ve done all the steps so far, you’ve managed to build an MCP server and solve a very practical problem: how to wrap an API, for a search AND make it “agentic”.