diff --git a/solutions/aisearch-mcp4aoai/.dockerignore b/solutions/aisearch-mcp4aoai/.dockerignore new file mode 100644 index 0000000..8db1a1e --- /dev/null +++ b/solutions/aisearch-mcp4aoai/.dockerignore @@ -0,0 +1,4 @@ +.env +.venv +docker_env +__pycache__/ \ No newline at end of file diff --git a/solutions/aisearch-mcp4aoai/.gitignore b/solutions/aisearch-mcp4aoai/.gitignore new file mode 100644 index 0000000..c66ba3a --- /dev/null +++ b/solutions/aisearch-mcp4aoai/.gitignore @@ -0,0 +1,2 @@ +.env +.venv \ No newline at end of file diff --git a/solutions/aisearch-mcp4aoai/Dockerfile b/solutions/aisearch-mcp4aoai/Dockerfile new file mode 100644 index 0000000..fd830ea --- /dev/null +++ b/solutions/aisearch-mcp4aoai/Dockerfile @@ -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"] \ No newline at end of file diff --git a/solutions/aisearch-mcp4aoai/ex_env_file b/solutions/aisearch-mcp4aoai/ex_env_file new file mode 100644 index 0000000..e581207 --- /dev/null +++ b/solutions/aisearch-mcp4aoai/ex_env_file @@ -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= \ No newline at end of file diff --git a/solutions/aisearch-mcp4aoai/readme.md b/solutions/aisearch-mcp4aoai/readme.md new file mode 100644 index 0000000..170c35b --- /dev/null +++ b/solutions/aisearch-mcp4aoai/readme.md @@ -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= +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 \ + --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.** \ No newline at end of file diff --git a/solutions/aisearch-mcp4aoai/requirements.txt b/solutions/aisearch-mcp4aoai/requirements.txt new file mode 100644 index 0000000..44546be --- /dev/null +++ b/solutions/aisearch-mcp4aoai/requirements.txt @@ -0,0 +1,10 @@ +mcp[cli] +openai +ipykernel +httpx +python-dotenv +azure-core +azure-search-documents +starlette +uvicorn +semantic-kernel \ No newline at end of file diff --git a/solutions/aisearch-mcp4aoai/server.py b/solutions/aisearch-mcp4aoai/server.py new file mode 100644 index 0000000..8c569d0 --- /dev/null +++ b/solutions/aisearch-mcp4aoai/server.py @@ -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 --image : . + +**COMMAND TO UPDATE THE CONTAINER APP TO USE THE NEW IMAGE: +az containerapp update --name --resource-group --image .azurecr.io/: + +''' + +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}") \ No newline at end of file diff --git a/solutions/aisearch-mcp4aoai/server_sse.py b/solutions/aisearch-mcp4aoai/server_sse.py new file mode 100644 index 0000000..5d7a653 --- /dev/null +++ b/solutions/aisearch-mcp4aoai/server_sse.py @@ -0,0 +1,284 @@ +''' +MCP (Model Context Protocol) Server with SSE (Server-Sent Events) Support + +This server provides tools to Large Language Models through the MCP protocol. +It uses SSE for real-time communication, which is compatible with OpenAI's SDK. + +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 + +Technical Note: SSE is deprecated in favor of streamable-http, but OpenAI SDK +hasn't updated yet, so we use Starlette to handle SSE requests. + +**COMMAND TO BUILD AND PUSH DOCKER IMAGE TO ACR: +az acr build --registry --image : . + +**COMMAND TO UPDATE THE CONTAINER APP TO USE THE NEW IMAGE: +az containerapp update --name --resource-group --image .azurecr.io/: + +''' + +# MCP (Model Context Protocol) imports +from mcp.server.fastmcp import FastMCP +from mcp.server import Server +from mcp.server.sse import SseServerTransport + +# Web framework imports +from starlette.applications import Starlette +from starlette.routing import Mount, Route +from starlette.requests import Request +import uvicorn + +# Standard library and utility imports +import argparse +from typing import Any +import httpx, os, random +import os +from dotenv import load_dotenv + +# Azure services imports +from openai import AzureOpenAI +from azure.core.credentials import AzureKeyCredential +from azure.search.documents import SearchClient +from azure.search.documents.models import VectorizedQuery + +# Load environment variables from .env file +load_dotenv() + +# Create the main MCP server instance +# FastMCP provides a simplified way to create MCP servers with decorators +mcp = FastMCP( + name="InfoHub", # This name identifies our server to MCP clients +) + + +# Tool 1: Simple Calculator +# The @mcp.tool() decorator registers this function as an available tool for LLMs +@mcp.tool() +def add(a: int, b: int) -> int: + """ + Add two numbers together. + + This is a simple example tool that demonstrates how to expose functions + to Large Language Models through the MCP protocol. + + Args: + a (int): First number to add + b (int): Second number to add + + Returns: + int: Sum of the two numbers + """ + return a + b + +# Tool 2: Azure AI Search Integration +# This tool allows LLMs to search through documents stored in Azure AI Search +@mcp.tool() +def ai_search(query: str) -> str: + """ + Retrieve documents from an Azure AI Search index using hybrid search. + + This function performs a sophisticated search that combines: + - Vector search (using embeddings for semantic similarity) + - Semantic search (using Azure's built-in semantic capabilities) + - Reranking (to improve result relevance) + + Args: + query (str): The search query string from the user + + Returns: + str: Formatted search results with content, scores, and metadata + """ + # Step 1: Get environment variables for Azure services + 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') + + # Step 2: Initialize Azure OpenAI client for generating embeddings + client = AzureOpenAI( + api_key=azure_openai_api_key, + api_version=azure_openai_api_version, + azure_endpoint=azure_endpoint + ) + + # Step 3: Convert the user's query into a vector representation + embedding_response = client.embeddings.create( + input=query, + model=azure_ada_deployment + ) + query_vector = embedding_response.data[0].embedding + + # Step 4: Initialize Azure AI Search client + search_client = SearchClient( + endpoint=search_endpoint, + index_name=search_index, + credential=AzureKeyCredential(search_key) + ) + + # Step 5: Create a vector query for similarity search + vector_query = VectorizedQuery( + vector=query_vector, + k_nearest_neighbors=5, + fields=search_vector_field_name, + exhaustive=True + ) + + # Step 6: Execute the hybrid search + 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_answer="extractive|count-5", + query_caption="extractive|highlight-false", + ) + + # Step 7: Process and format the search results + i = 0 + final_output = "" + for result in results: + # Get the reranker score (measures relevance quality) + reranker_score = result.get('@search.reranker_score', 0) + + # Only include results with good reranker scores (>=1.5) + if reranker_score >= 1.5: + i += 1 + content = result.get(search_content_field_name, '') + score = result.get('@search.score', 'N/A') + + # Format the result in a readable way + 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 + + +# Web Application Setup +# This section creates a web application that can handle MCP connections over HTTP + +def create_starlette_app(mcp_server: Server, *, debug: bool = False) -> Starlette: + """ + Create a Starlette web application that serves the MCP server over SSE. + + This function sets up the web infrastructure needed to expose our MCP tools + to clients (like LLMs) over HTTP using Server-Sent Events (SSE). + + Args: + mcp_server: The MCP server instance containing our tools + debug: Whether to enable debug mode (more verbose logging) + + Returns: + Starlette: A configured web application ready to serve MCP requests + """ + # Create SSE transport layer + # SSE allows real-time bidirectional communication between client and server + # The "/messages/" path is where MCP protocol messages will be sent + sse = SseServerTransport("/messages/") + + class SSEEndpoint: + """ + ASGI-compatible endpoint for handling SSE connections. + + This class implements the ASGI callable protocol, which means it can + be used directly as a route endpoint in Starlette. It handles the + SSE connection setup and delegates MCP communication to the server. + """ + + def __init__(self, mcp_server: Server, sse_transport: SseServerTransport): + """ + Initialize the SSE endpoint. + + Args: + mcp_server: The MCP server that will handle tool requests + sse_transport: The SSE transport layer for communication + """ + self.mcp_server = mcp_server + self.sse_transport = sse_transport + + async def __call__(self, scope, receive, send): + """ + Handle incoming SSE connections (ASGI callable protocol). + + This method is called when a client connects to the /sse endpoint. + It establishes the SSE connection and runs the MCP server to handle + tool requests from the client. + + Args: + scope: ASGI scope containing request information + receive: ASGI receive function for getting messages + send: ASGI send function for sending responses + """ + print(f"{'-'*50}\nhandling sse\n{'-'*50}") # Log when a new SSE connection is established + + # Establish SSE connection and run MCP server + # The context manager handles connection setup/teardown automatically + async with self.sse_transport.connect_sse(scope, receive, send) as (read_stream, write_stream): + # Run the MCP server with the established streams + # This is where the actual MCP protocol communication happens + await self.mcp_server.run( + read_stream, # Stream for receiving messages + write_stream, # Stream for sending responses + self.mcp_server.create_initialization_options(), # Server configuration + ) + + # Create and configure the Starlette web application + return Starlette( + debug=debug, # Enable debug mode if requested + routes=[ + # Route for SSE connections - this is where clients connect for real-time communication + Route("/sse", endpoint=SSEEndpoint(mcp_server, sse)), + + # Route for MCP message handling - this handles the actual protocol messages + # Mount means all URLs under /messages/ will be handled by this app + Mount("/messages/", app=sse.handle_post_message), + ], + ) + + +# Server Startup Section +# This section handles starting the server when the script is run directly + +if __name__ == "__main__": + # Get the underlying MCP server instance from FastMCP + # FastMCP is a wrapper that simplifies MCP server creation + mcp_server = mcp._mcp_server + + # Create the web application that will serve our MCP server + # This wraps our MCP server in a web interface accessible over HTTP + starlette_app = create_starlette_app(mcp_server, debug=True) + + # Set up command-line argument parsing + # This allows users to customize host and port when running the server + parser = argparse.ArgumentParser(description='Run MCP SSE-based server') + parser.add_argument('--host', default='0.0.0.0', + help='Host to bind to (0.0.0.0 means accept connections from any IP)') + parser.add_argument('--port', type=int, default=8080, + help='Port to listen on (default: 8080)') + args = parser.parse_args() + + # Start the web server + # Uvicorn is an ASGI server that can run our Starlette application + print(f"Starting MCP server on {args.host}:{args.port}") + print(f"Available tools: add, ai_search") + print(f"SSE endpoint: http://{args.host}:{args.port}/sse") + print(f"Messages endpoint: http://{args.host}:{args.port}/messages/") + + uvicorn.run(starlette_app, host=args.host, port=args.port) \ No newline at end of file diff --git a/solutions/aisearch-mcp4aoai/test_mcp_server/openai_sdk.py b/solutions/aisearch-mcp4aoai/test_mcp_server/openai_sdk.py new file mode 100644 index 0000000..87cd277 --- /dev/null +++ b/solutions/aisearch-mcp4aoai/test_mcp_server/openai_sdk.py @@ -0,0 +1,121 @@ +''' + +This code was referenced from https://github.com/Azure-Samples/AI-Gateway/blob/main/labs/model-context-protocol/model-context-protocol.ipynb + +The script connects to an MCP tool server, lists available tools, and uses Azure OpenAI to determine which tools to invoke based on a given prompt. +It then calls the appropriate tools and completes the response using the tool outputs. + +**Make sure to replace**: +- Lines 64 & 100 with the name of your deployed model +- Line 122 with a relevant query to your Azure AI Search Index + +''' +import json +import asyncio +import os +from mcp import ClientSession +from mcp.client.sse import sse_client +from openai import AzureOpenAI +from dotenv import load_dotenv + +load_dotenv() + +async def call_tool(mcp_session, function_name, function_args): + try: + func_response = await mcp_session.call_tool(function_name, function_args) + func_response_content = func_response.content + except Exception as e: + func_response_content = json.dumps({"error": str(e)}) + return str(func_response_content) + +async def run_completion_with_tools(server_url, prompt): + streams = None + session = None + try: + # Connect to MCP tool server + streams_ctx = sse_client(server_url) + streams = await streams_ctx.__aenter__() + session_ctx = ClientSession(streams[0], streams[1]) + session = await session_ctx.__aenter__() + await session.initialize() + response = await session.list_tools() + tools = response.tools + print(f"✅ Connected to server {server_url}") + + # Build OpenAI-compatible tools spec + openai_tools = [{ + "type": "function", + "function": { + "name": tool.name, + "parameters": tool.inputSchema + }, + } for tool in tools] + + # Step 1: Ask the model which function to call + print("▶️ Step 1: start a completion to identify the appropriate functions to invoke based on the prompt") + client = AzureOpenAI( + azure_endpoint=os.getenv("AZURE_OPENAI_ENDPOINT"), + api_key=os.getenv("AZURE_OPENAI_KEY"), + api_version=os.getenv("AZURE_OPENAI_VERSION"), + ) + + messages = [{"role": "user", "content": prompt}] + response = client.chat.completions.create( + model="your_deployed_model_name", # USE your deployment name here + messages=messages, + tools=openai_tools, + store=True, + metadata={ + "user": "admin", + "category": "mcp-test", + } + ) + + response_message = response.choices[0].message + tool_calls = response_message.tool_calls + print(f"Tools from step 1: response: {response_message}, tool_calls: {tool_calls}\n") + + if tool_calls: + # Step 2: Call the tools + messages.append(response_message) + print("▶️ Step 2: call the functions") + for tool_call in tool_calls: + function_name = tool_call.function.name + function_args = json.loads(tool_call.function.arguments) + print(f" Function Name: {function_name} | Args: {function_args}") + + function_response = await call_tool(session, function_name, function_args) + print(f" Function Response: {function_response}") + + messages.append({ + "tool_call_id": tool_call.id, + "role": "tool", + "name": function_name, + "content": function_response, + }) + + # Step 3: Let the model finish the response + print("▶️ Step 3: finish with a completion to answer the user prompt using the function response") + second_response = client.chat.completions.create( + model="your_deployed_model_name", # USE your deployment name here + messages=messages, + store=True, + metadata={ + "user": "admin", + "category": "mcp-test", + } + ) + print("💬", second_response.choices[0].message.content) + else: + print("⚠️ No tools were called by the model.") + + except Exception as e: + print(f"❌ Error: {e}") + finally: + if session: + await session_ctx.__aexit__(None, None, None) + if streams: + await streams_ctx.__aexit__(None, None, None) + +# Run the workflow +asyncio.run(run_completion_with_tools("http://0.0.0.0:8080/sse", "")) diff --git a/solutions/aisearch-mcp4aoai/test_mcp_server/semantic_kernel_agent.py b/solutions/aisearch-mcp4aoai/test_mcp_server/semantic_kernel_agent.py new file mode 100644 index 0000000..cafe11a --- /dev/null +++ b/solutions/aisearch-mcp4aoai/test_mcp_server/semantic_kernel_agent.py @@ -0,0 +1,56 @@ +''' + +This code was referenced from https://github.com/Azure-Samples/AI-Gateway/blob/main/labs/model-context-protocol/model-context-protocol.ipynb + + +The script sets up an AI agent using Azure OpenAI and MCP SSE Plugin to respond to user queries based on a given prompt. +It demonstrates how to create an agent, sync with your MCP cilent, invoke it for a response, and clean up the session. + +**Make sure to replace**: +- Line 37 with the name of your deployed model +- Line 24 with a relevant query to your Azure AI Search Index + +''' + +import asyncio +from semantic_kernel.agents import ChatCompletionAgent, ChatHistoryAgentThread +from semantic_kernel.connectors.ai.open_ai import AzureChatCompletion +from semantic_kernel.connectors.mcp import MCPSsePlugin +import os +from dotenv import load_dotenv +load_dotenv() + +user_input = "" + +async def main(): + # 1. Create the agent + async with MCPSsePlugin( + name="AzureAiSearch", + url=f"http://0.0.0.0:8080/sse", + description="Azure AI Search Plugin", + ) as aisearch_plugin: + agent = ChatCompletionAgent( + service=AzureChatCompletion( + endpoint=os.getenv("AZURE_OPENAI_ENDPOINT"), + api_key=os.getenv("AZURE_OPENAI_KEY"), + api_version=os.getenv("AZURE_OPENAI_VERSION"), + deployment_name="your_deployed_model_name" # USE your deployment name here + ), + name="MCPAgent", + instructions="Answer questions using the tools provided.", + plugins=[aisearch_plugin], + ) + + thread: ChatHistoryAgentThread | None = None + + print(f"# User: {user_input}") + # 2. Invoke the agent for a response + response = await agent.get_response(messages=user_input, thread=thread) + print(f"# {response.name}: {response} ") + thread = response.thread # type: ignore + + # 3. Cleanup: Clear the thread + await thread.delete() if thread else None + +if __name__ == "__main__": + asyncio.run(main()) \ No newline at end of file