Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions solutions/aisearch-mcp4aoai/.dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
.env
.venv
docker_env
__pycache__/
2 changes: 2 additions & 0 deletions solutions/aisearch-mcp4aoai/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
.env
.venv
22 changes: 22 additions & 0 deletions solutions/aisearch-mcp4aoai/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
FROM python:3.11-slim

WORKDIR /app

# Install uv for faster package management
RUN pip install uv

# Copy requirements file
COPY requirements.txt .

# Install dependencies using uv
RUN uv venv
RUN uv pip install -r requirements.txt

# Copy application code
COPY server_sse.py .

# Expose the port the server runs on
EXPOSE 8080

# Command to run the server
CMD ["uv", "run", "server_sse.py"]
10 changes: 10 additions & 0 deletions solutions/aisearch-mcp4aoai/ex_env_file
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
AZURE_SEARCH_ENDPOINT=
AZURE_SEARCH_KEY=
AZURE_SEARCH_INDEX=
AZURE_SEARCH_VECTOR_FIELD_NAME=
AZURE_SEARCH_CONTENT_FIELD_NAME=
AZURE_SEARCH_SEMANTIC_CONFIGURATION_NAME=
AZURE_OPENAI_ENDPOINT=
AZURE_OPENAI_KEY=
AZURE_OPENAI_VERSION=
AZURE_EMBEDDINGS_DEPLOYMENT=
125 changes: 125 additions & 0 deletions solutions/aisearch-mcp4aoai/readme.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
# MCP Project Overview

## What is This Project?

This project demonstrates how to expose custom tools as MCP servers to Large Language Models using the **Model Context Protocol (MCP)**. It enables the LLMs (such as Azure OpenAI GPT models) to call backend Python functions, including Azure AI Search and a basic calculator, via a standardized protocol. The project includes both server and client/host scripts, and is designed for easy deployment to Azure using Azure Container Registry and Azure Container Apps.

---

## Key Features

- **MCP Server**: Exposes Python functions as tools for LLMs, including a calculator and Azure AI Search integration.
- **SSE & HTTP Support**: Real-time communication using Server-Sent Events (SSE), compatible with OpenAI SDK.
- **Azure AI Search Integration**: Enables LLMs to retrieve and reason over enterprise data indexed in Azure AI Search.
- **Test Scripts**: Includes OpenAI SDK-based test clients to simulate LLM tool-calling workflows.

---

## Project Structure

- [`server.py`](server.py): FastMCP server exposing tools (calculator, AI Search) using streamable-http transport.
- [`server_sse.py`](server_sse.py): MCP server exposing tools (calculator, AI Search) using Starlette and SSE ***for OpenAI SDK compatibility***.
- [`test_mcp_server/openai_sdk.py`](test_mcp_server/openai_sdk.py): Test script that simulates an LLM calling MCP tools via OpenAI SDK.
- [`test_mcp_server/semantic_kernel_agent.py`](test_mcp_server/semantic_kernel_agent.py): Example of using Semantic Kernel with MCP plugins.
- [`requirements.txt`](requirements.txt): Python dependencies for the MCP server and test scripts.
- [`Dockerfile`](Dockerfile): Container definition for deployment.
- [`ex_env_file`](ex_env_file): Example environment variables for local or container deployment.

---

## How It Works

1. **MCP Server** exposes tools (e.g., `add`, `ai_search`) via HTTP/SSE endpoints.
2. **LLM (Azure OpenAI)** is prompted with a user query and a list of available tools.
3. **LLM decides** which tool(s) to call, and the client script (e.g., [`openai_sdk.py`](test_mcp_server/openai_sdk.py)) orchestrates the tool invocation.
4. **Tool results** are returned to the LLM, which then generates a final response for the user.

---

## How to Deploy to Azure

### 1. Build and Push Docker Image to Azure Container Registry (ACR)

```sh
# Log in to Azure
az login

# Set variables
ACR_NAME=<your-acr-name>
IMAGE_NAME=mcp-server
TAG=latest

# Build and push image to ACR
az acr build --registry $ACR_NAME --image $IMAGE_NAME:$TAG .
```

### 2. Deploy to Azure Container Apps

```sh
# Update the container app to use the new image
az containerapp update \
--name <your-container-app-name> \
--resource-group <your-resource-group> \
--image $ACR_NAME.azurecr.io/$IMAGE_NAME:$TAG
```

- Make sure to set environment variables in the Container App settings (see `.env_example` for required keys).

---

## Environment Variables

Copy and fill in [`ex_env_file`](ex_env_file) as `.env` for local development or set these as application settings in Azure.

Key variables include:
- `AZURE_SEARCH_ENDPOINT`
- `AZURE_SEARCH_KEY`
- `AZURE_SEARCH_INDEX`
- `AZURE_SEARCH_VECTOR_FIELD_NAME`
- `AZURE_OPENAI_ENDPOINT`
- `AZURE_OPENAI_KEY`
- `AZURE_OPENAI_VERSION`
- `AZURE_EMBEDDINGS_DEPLOYMENT`

---

## Running Locally

You can use [uv](https://github.com/astral-sh/uv) as a fast, reliable drop-in replacement for `pip` and `python` commands below.

1. **Install dependencies** (recommended: use `uv`):

```sh
uv pip install -r requirements.txt
# or, with pip:
pip install -r requirements.txt
```

2. **Set up your `.env` file** (copy from `.env_example` and fill in values).

3. **Start the server** (for example, with SSE support):

```sh
uv python server_sse.py
# or, with python:
python server_sse.py
```

4. **Run test scripts** (in another terminal):

```sh
uv python test_mcp_server/openai_sdk.py
# or, with python:
python test_mcp_server/openai_sdk.py
```

---

## References

- [MCP Protocol GitHub](https://github.com/microsoft/mcp-for-beginners)
- [OpenAI Function Calling](https://platform.openai.com/docs/guides/function-calling)

---

**This project enables secure, scalable, and extensible tool-calling for LLMs in enterprise environments using Azure.**
10 changes: 10 additions & 0 deletions solutions/aisearch-mcp4aoai/requirements.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
mcp[cli]
openai
ipykernel
httpx
python-dotenv
azure-core
azure-search-documents
starlette
uvicorn
semantic-kernel
130 changes: 130 additions & 0 deletions solutions/aisearch-mcp4aoai/server.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
'''
MCP (Model Context Protocol) Server with Streamable-HTTP Support

This server provides tools to Large Language Models through the MCP protocol.
It uses streamable-http for real-time communication, which is NOT compatible with OpenAI's SDK to date.

Features:
- Simple calculator tool (add function)
- Azure AI Search integration for retrieving documents
- SSE transport for real-time communication
- Starlette web framework for HTTP handling

**COMMAND TO BUILD AND PUSH DOCKER IMAGE TO ACR:
az acr build --registry <your-acr-name> --image <your-image-name>:<tag> .

**COMMAND TO UPDATE THE CONTAINER APP TO USE THE NEW IMAGE:
az containerapp update --name <your-container-app-name> --resource-group <your-resource-group> --image <your-acr-name>.azurecr.io/<your-image-name>:<tag>

'''

from mcp.server.fastmcp import FastMCP
import os
from dotenv import load_dotenv
from openai import AzureOpenAI
from azure.core.credentials import AzureKeyCredential
from azure.search.documents import SearchClient
from azure.search.documents.models import VectorizedQuery

load_dotenv()

# Create an MCP server
mcp = FastMCP(
name="Calculator",
host="0.0.0.0", # only used for SSE transport (localhost)
port=8080, # only used for SSE transport (set this to any port)
)


# Add a simple calculator tool
@mcp.tool()
def add(a: int, b: int) -> int:
"""Add two numbers together"""
return a + b

# Add a tool to retrieve documents from an Azure AI Search index
@mcp.tool()
def ai_search(query: str) -> str:
"""
Retrieve documents from an Azure AI Search index.

Args:
query (str): The search query string.

Returns:
str: A list of documents matching the query in a string.
"""
# Set env variables
search_endpoint = os.getenv("AZURE_SEARCH_ENDPOINT")
search_key = os.getenv("AZURE_SEARCH_KEY")
search_index = os.getenv("AZURE_SEARCH_INDEX")
search_vector_field_name = os.getenv("AZURE_SEARCH_VECTOR_FIELD_NAME")
search_content_field_name = os.getenv("AZURE_SEARCH_CONTENT_FIELD_NAME")
search_semantic_configuration_name = os.getenv("AZURE_SEARCH_SEMANTIC_CONFIGURATION_NAME")`
azure_endpoint = os.getenv('AZURE_OPENAI_ENDPOINT')
azure_openai_api_key = os.getenv('AZURE_OPENAI_KEY')
azure_openai_api_version = os.getenv('AZURE_OPENAI_VERSION')
azure_ada_deployment = os.getenv('AZURE_EMBEDDINGS_DEPLOYMENT')
# Initialize the AzureOpenAI client
client = AzureOpenAI(
api_key=azure_openai_api_key,
api_version=azure_openai_api_version,
azure_endpoint=azure_endpoint
)
# Get the vector representation of the user query using the Ada model
embedding_response = client.embeddings.create(
input=query,
model=azure_ada_deployment
)
query_vector = embedding_response.data[0].embedding
# Create a SearchClient
search_client = SearchClient(endpoint=search_endpoint,
index_name=search_index,
credential=AzureKeyCredential(search_key))
vector_query = VectorizedQuery(vector=query_vector, k_nearest_neighbors=5, fields=search_vector_field_name, exhaustive=True)
# Query the index
results = search_client.search(
search_text=None,
vector_queries= [vector_query],
top=5,
select="*",
semantic_query=query,
query_type="semantic",
semantic_configuration_name=search_semantic_configuration_name, # Ensure this matches your index configuration
# query_language="en-us",
query_answer="extractive|count-5",
query_caption="extractive|highlight-false",
)
# Print the results
i = 0
final_output = ""
for result in results:
reranker_score = result.get('@search.reranker_score', 0) # Default to 0 if not present
if reranker_score >= 1.5: # Filter by reranker score
i += 1
content = result.get(search_content_field_name, '')
score = result.get('@search.score', 'N/A')
final_output += (
f"Source {i}\n"
f"Content: {content}\n"
f"@search.score: {score}\n"
f"@search.reranker_score: {reranker_score}\n"
f"{'-'*50}\n\n"
)
return final_output





# Run the server
if __name__ == "__main__":
transport = "streamable-http"
if transport == "stdio":
print("Running server with stdio transport")
mcp.run(transport="stdio")
elif transport == "streamable-http":
print("Running server with SSE transport")
mcp.run(transport="streamable-http")
else:
raise ValueError(f"Unknown transport: {transport}")
Loading