Blog Post

Microsoft Developer Community Blog
6 MIN READ

Building an MCP Server for Microsoft Learn

Chris_Noring's avatar
Chris_Noring
Icon for Microsoft rankMicrosoft
Jun 18, 2025

So 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.

So 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.

 

> DISCLAIMER, the below article is just to show you how easy you can wrap an API and make that into an MCP Server. There's now an official MCP Server for Docs and Learn which you're encouraged to use over building one for Docs and Learn yourself, see this link to the official server https://github.com/MicrosoftDocs/mcp

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_filterfree_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:

.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:

.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:

.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 `venv\Scripts\activate`

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".

Learn more

Updated Jun 18, 2025
Version 2.0
No CommentsBe the first to comment