Five AI projects, each small enough for one weekend and each backed by a real Azure service: chat over your own PDFs (AI Search + a Foundry model), a support bot on Azure Functions that hands off to a person, a cloud bill explainer on the Cost Management API, a remote MCP server on Azure Functions, and an invoice reader on Document Intelligence. Every section has the services, the steps, the code that matters, and what to put in the README so an interviewer can open it and try it.
🎯 The five at a glance
| # | Project | Core services | Rough build time | What it shows an interviewer |
|---|---|---|---|---|
| 1 | Chat over your documents | Blob Storage, AI Search, Foundry chat model | 3 to 4 hours | You can ground a model in your own data and cite sources |
| 2 | Support bot with handoff | Azure Functions, Foundry chat model, Storage Queue | 2 hours | You design for the model being wrong |
| 3 | Cloud bill explainer | Cost Management Query API, Foundry chat model | 1 to 2 hours | You can turn real billing data into something a manager reads |
| 4 | MCP server | Azure Functions (MCP extension), Entra auth | 2 hours | You can make a tool callable by an AI assistant, securely |
| 5 | Invoice reader | Blob Storage, Document Intelligence, Azure Functions | 2 to 3 hours | You can build an event-driven pipeline that extracts structured data |
✅ Rules for all five
| Rule | Why |
|---|---|
| Set a budget alert on the subscription before you deploy anything | AI Search and model deployments are the two things that surprise people |
| Use managed identity, never an API key in code or app settings | It is the first thing a reviewer looks for, and it is easier once you do it |
| One resource group per project | Clean teardown, clean cost view |
| Pin region once and reuse it | Model availability varies by region, and cross-region calls add latency |
| README: architecture in one diagram, how to run it, and one thing that broke | The broken thing is the part that sounds like real work |
1️⃣ Project 1: Chat over your own documents
Load PDFs into Azure AI Search and answer questions about them with a Foundry chat model, with citations.
The pattern is retrieval augmented generation (RAG): split documents into chunks, store them in a search index, pull the best chunks for a question, and give those chunks to the model as context.
🏗️ Architecture
| Layer | Service |
|---|---|
| Documents | Blob Storage container with 5 to 10 PDFs you wrote or own |
| Search and index | Azure AI Search, chunking and embeddings built into the indexer |
| Models | A small chat model and an embedding model deployed in a Microsoft Foundry project (called Azure AI Foundry in older docs) |
| App | The Microsoft sample app, or your own thin API |
🛠️ Two routes
| Route | Best for | Time |
|---|---|---|
| A: Start from the sample | Getting a working app with a UI, then making it yours | Under an hour to deploy, then your own data |
| B: Build the pipeline in the portal | Understanding every moving part | 3 to 4 hours |
Route A
azd init -t azure-search-openai-demo
azd auth login
azd upThe repo is Azure-Samples/azure-search-openai-demo. Swap its sample PDFs for your own by following the README section on adding documents, then redeploy. The portfolio value is in what you change: your documents, your system prompt, and a README that explains each component.
Route B
- Create a resource group, a storage account, and a container. Upload your PDFs.
- Create an Azure AI Search service. Use the Free tier for a handful of PDFs. A Basic tier bills while it exists, so delete it when you finish.
- In a Foundry project, deploy a small chat model and an embedding model.
- In the AI Search portal, run the Import data wizard against the container with vectorization on. It creates the data source, skillset, index, and indexer.
- Assign roles (below), then open the chat playground, choose Add your data, pick your index, set the search type to Semantic, and authenticate with the system assigned managed identity.
- Set a system prompt that forces citations, then test.
Answer the question based on the context below. Be specific and cite the source file name in brackets for each fact.🔐 Roles you will need
| Who | Role | On what |
|---|---|---|
| You, in the playground | Cognitive Services OpenAI User | The Azure OpenAI resource |
| The Azure OpenAI managed identity | Search Service Contributor | The AI Search service |
| The Azure OpenAI managed identity | Search Index Data Reader | The AI Search service |
🧯 When it breaks
| Symptom | Likely cause | Fix |
|---|---|---|
PermissionDenied in the playground | Your user is missing the OpenAI User role | Assign Cognitive Services OpenAI User on the OpenAI resource |
403 reading /indexes/{name} | The OpenAI managed identity cannot read the index | Assign Search Service Contributor on the search service |
| Answers with no citations | Wrong search type or semantic configuration | Select Semantic and match the configuration name |
| Document count is zero after the wizard | Indexer run failed or the path is wrong | Check the indexer run history for the error |

Create Your First Azure AI Search Index
The index is the half of RAG people skip. Build one by hand before you put a model on top of it.
2️⃣ Project 2: A support bot that knows when to hand off
An Azure Function that answers from a short FAQ, and passes the question to a person when the model is not sure.
The handoff is the project. Anyone can wire a model to an endpoint. A bot that admits it does not know is the one a business would actually deploy.
🏗️ Architecture
| Layer | Service |
|---|---|
| Endpoint | Azure Functions, HTTP trigger, Python v2 model |
| Model | Foundry chat model, called with the function's managed identity |
| Handoff | Storage Queue output binding, which a person or a second function reads |
🛠️ Build steps
- Create a function app and deploy a chat model in a Foundry project.
- Turn on the function app's system assigned identity and give it Cognitive Services OpenAI User on the OpenAI resource.
- Add app settings for the endpoint and the deployment name.
- Write the function below, test with
func start, then publish.
az functionapp identity assign -g rg-support-bot -n func-support-bot
az role assignment create \
--assignee <principalId-from-previous-command> \
--role "Cognitive Services OpenAI User" \
--scope <openai-resource-id>💻 The function
import json, os
import azure.functions as func
from azure.identity import DefaultAzureCredential, get_bearer_token_provider
from openai import AzureOpenAI
app = func.FunctionApp()
client = AzureOpenAI(
azure_endpoint=os.environ["AZURE_OPENAI_ENDPOINT"],
azure_ad_token_provider=get_bearer_token_provider(
DefaultAzureCredential(), "https://cognitiveservices.azure.com/.default"
),
api_version="2024-10-21",
)
FAQ = open("faq.md").read()
SYSTEM = (
"You are a support assistant. Answer only from the FAQ below. "
'Reply as JSON: {"answer": string, "confident": boolean}. '
"Set confident to false if the FAQ does not clearly answer the question.\n\n"
+ FAQ
)
@app.route(route="ask", methods=["POST"], auth_level=func.AuthLevel.FUNCTION)
@app.queue_output(arg_name="handoff", queue_name="handoff", connection="AzureWebJobsStorage")
def ask(req: func.HttpRequest, handoff: func.Out[str]) -> func.HttpResponse:
question = req.get_json()["question"]
r = client.chat.completions.create(
model=os.environ["CHAT_DEPLOYMENT"], # the deployment name, not the model name
messages=[
{"role": "system", "content": SYSTEM},
{"role": "user", "content": question},
],
response_format={"type": "json_object"},
temperature=0,
)
out = json.loads(r.choices[0].message.content)
if not out.get("confident"):
handoff.set(question) # a person picks it up from the queue
return func.HttpResponse(json.dumps({"handoff": True}), mimetype="application/json")
return func.HttpResponse(
json.dumps({"handoff": False, "answer": out["answer"]}), mimetype="application/json"
)🔁 Make the handoff real
| Option | Effort | Result |
|---|---|---|
| Queue only | None | Questions pile up in handoff, which you can read in Storage Explorer |
| Second function with a queue trigger | Low | Posts the question to Teams or email |
| Log handoff rate | Low | A number you can put in the README: how often the bot said "I don't know" |
| Sample the confident answers | Low | The model's own confident flag is a weak signal, so read a handful each week to see whether it means anything |
Before you share the URL, check the auth level.
3️⃣ Project 3: A cloud bill explainer
Pull last month's costs from Cost Management and have a model explain the top three line items in plain English.
This is the quickest of the five and the one a hiring manager understands immediately, because everyone has stared at a bill and wondered what it meant.
🏗️ Architecture
| Layer | Service |
|---|---|
| Data | Cost Management Query API, grouped by service |
| Access | Cost Management Reader on the subscription |
| Explainer | Foundry chat model |
🛠️ Pull the data
SUB=$(az account show --query id -o tsv)
az rest --method post \
--url "https://management.azure.com/subscriptions/$SUB/providers/Microsoft.CostManagement/query?api-version=2023-11-01" \
--body '{
"type": "ActualCost",
"timeframe": "TheLastMonth",
"dataset": {
"granularity": "None",
"aggregation": { "totalCost": { "name": "Cost", "function": "Sum" } },
"grouping": [ { "type": "Dimension", "name": "ServiceName" } ]
}
}' > last-month.json💻 Rank in code, explain with the model
import json
data = json.load(open("last-month.json"))["properties"]
cols = [c["name"] for c in data["columns"]]
rows = [dict(zip(cols, r)) for r in data["rows"]]
top3 = sorted(rows, key=lambda r: r["Cost"], reverse=True)[:3]
prompt = (
"Explain each of these three Azure charges to a non-technical manager. "
"For each one say what the service does, why the cost could be this size, "
"and one thing to check. Do not invent numbers.\n\n" + json.dumps(top3)
)
# send `prompt` with the same client setup as Project 2🧯 Things that bite
| Issue | What to do |
|---|---|
| The numbers look slightly stale | Cost data for the open period refreshes about every four hours |
429 responses | The Query API is rate limited, so cache the result and retry with a backoff |
| Empty result | Check you have Cost Management Reader at the scope you queried |
| Sensitive bill | Group by service only. Do not send resource names or IDs to the model |
4️⃣ Project 4: An MCP server for a tool you already use
Wrap one tool or API you already use so an AI assistant can call it, and host it as a remote MCP server on Azure Functions.
MCP (Model Context Protocol) is how an assistant like GitHub Copilot calls your tools. A remote server hosted in Azure with Entra sign-in shows you can do the part that matters in a real company: expose something to an AI safely.
🏗️ Architecture
| Layer | Service |
|---|---|
| Server | Azure Functions with the MCP extension, Flex Consumption plan |
| Auth | Built-in MCP authorization with Microsoft Entra, on by default in the template |
| Client | GitHub Copilot in VS Code, through .vscode/mcp.json |
🛠️ Build steps
azd init --template remote-mcp-functions-python -e mcpserver-python
azd up
azd env get-value AZURE_FUNCTION_NAME- Start Azurite and run the project locally with Core Tools. Test it from Copilot using the
local-mcp-functionentry in.vscode/mcp.json. - Each tool is a function in
src/function_app.pymarked with the MCP annotation. Copy one of the existing tools and replace the body with a call to the API you already use. - Run
azd up, then start theremote-mcp-functionentry and sign in with Entra when prompted. - Run
azd downwhen you are done.
The template repo is Azure-Samples/remote-mcp-functions-python. Check its README for the Python version and Core Tools version it needs.
| Detail | Value |
|---|---|
| Extension server endpoint | https://<FUNCTION_APP_NAME>.azurewebsites.net/runtime/webhooks/mcp |
| Plan | Flex Consumption, pay for what you use |
| Typical quickstart cost | A few US cents or less |
🔐 Auth and scope
The template turns on built-in MCP authorization with Entra, so leave it on and sign in when you connect. Then pick what the server is allowed to do.
Once a read-only tool works from Copilot, the project is done. The lab below walks the same pattern from scratch.

Build Interactive MCP Apps with Azure Functions
Build and deploy an MCP server on Azure Functions end to end, then point your own tool at it.
5️⃣ Project 5: An invoice reader
Drop invoices into Blob Storage and pull vendor, date, and total out with Document Intelligence.
An event-driven pipeline with a structured output. It is also useful: the same shape handles receipts, forms, and contracts.
🏗️ Architecture
| Layer | Service |
|---|---|
| Input | Blob Storage container invoices |
| Trigger | Azure Functions blob trigger, event-based |
| Extraction | Document Intelligence prebuilt-invoice model |
| Output | A JSON file per invoice in a results container |
🛠️ Build steps
- Create a storage account with two containers,
invoicesandresults. - Create a Document Intelligence resource. The free tier is enough for testing.
- Give the function app's identity Cognitive Services User on the Document Intelligence resource.
- Deploy the function, then add an Event Grid event subscription on the storage account that points at the function. The endpoint looks like this, and the blob extension key comes from the function app's system keys:
https://<FUNCTION_APP_NAME>.azurewebsites.net/runtime/webhooks/blobs?functionName=Host.Functions.read_invoice&code=<BLOB_EXTENSION_KEY>- Upload a sample invoice and read the JSON that appears in
results.
💻 The function
import json, os
import azure.functions as func
from azure.identity import DefaultAzureCredential
from azure.ai.documentintelligence import DocumentIntelligenceClient
from azure.ai.documentintelligence.models import AnalyzeDocumentRequest
app = func.FunctionApp()
di = DocumentIntelligenceClient(os.environ["DI_ENDPOINT"], DefaultAzureCredential())
FIELDS = ("VendorName", "InvoiceId", "InvoiceDate", "InvoiceTotal")
@app.blob_trigger(arg_name="blob", path="invoices/{name}",
connection="AzureWebJobsStorage", source="EventGrid")
@app.blob_output(arg_name="out", path="results/{name}.json",
connection="AzureWebJobsStorage")
def read_invoice(blob: func.InputStream, out: func.Out[str]):
poller = di.begin_analyze_document(
"prebuilt-invoice", AnalyzeDocumentRequest(bytes_source=blob.read())
)
result = poller.result()
if not result.documents:
out.set(json.dumps({"error": "no invoice found"}))
return
fields = result.documents[0].fields
out.set(json.dumps({
k: {
"value": fields[k].get("content") if k in fields else None,
"confidence": fields[k].get("confidence") if k in fields else None,
}
for k in FIELDS
}))🧯 Things that bite
| Issue | What to do |
|---|---|
| Blob trigger never fires on Flex Consumption | Flex Consumption supports only the event-based blob trigger, which needs source="EventGrid" and an event subscription on the storage account. Container polling is not supported there |
| Slow trigger on a Consumption plan app | Polling triggers can take up to ten minutes to notice a new blob |
| Authentication fails with the identity | Use the endpoint shown on the resource's Keys and Endpoint page, and confirm the role assignment has finished propagating |
| A wrong total slips through | Store the confidence next to every value and flag anything below your threshold for review |

Analyze Forms and Documents with Azure AI Document Intelligence
Run the prebuilt models and read the output before you build the pipeline around them.
🧹 Cleanup
Delete each project's resource group when you finish. Deleting a resource group removes everything inside it.
az group delete --name rg-docs-chat --yes --no-wait
az group delete --name rg-support-bot --yes --no-wait
az group delete --name rg-bill-explainer --yes --no-wait
az group delete --name rg-invoice-reader --yes --no-wait
azd down # in the MCP project folder| Project | What keeps billing if you forget |
|---|---|
| 1 | AI Search on a paid tier, and the model deployments if they are provisioned |
| 2 | The function app and the storage account |
| 3 | Almost nothing, which is why it is the safe one to leave running |
| 4 | The Flex Consumption app and its storage |
| 5 | The storage account and any paid Document Intelligence tier |
📝 What goes in each README
| Section | One line to include |
|---|---|
| Architecture | One diagram with every service and the identity between them |
| Run it | The exact commands, tested on a clean machine |
| Cost | What it costs to leave running for a month, and how you tore it down |
| Security | How it authenticates, and which keys you did not use |
| What broke | The one error you hit and how you found the cause |