├── .gitignore ├── LICENSE ├── README.md ├── azure_ai_agent_service_server.py ├── azure_search_server.py ├── images └── demo.gif └── requirements.txt /.gitignore: -------------------------------------------------------------------------------- 1 | # .gitignore 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | *.so 6 | .Python 7 | env/ 8 | .env 9 | .venv 10 | venv/ 11 | ENV/ 12 | .uv/ 13 | .idea/ 14 | .DS_Store 15 | .venv -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/farzad528/mcp-server-azure-ai-agents/c9458ac3301ea5a745295264e3fd353bb30555d7/LICENSE -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Azure AI Agent Service + Azure AI Search MCP Server 2 | 3 | A Model Context Protocol (MCP) server that enables Claude Desktop to search your content using Azure AI services. Choose between Azure AI Agent Service (with both document search and web search) or direct Azure AI Search integration. 4 | 5 | ![demo](images/demo.gif) 6 | 7 | --- 8 | 9 | ## Overview 10 | 11 | This project provides two MCP server implementations to connect Claude Desktop with Azure search capabilities: 12 | 13 | 1. **Azure AI Agent Service Implementation (Recommended)** - Uses the powerful Azure AI Agent Service to provide: 14 | - **Azure AI Search Tool** - Search your indexed documents with AI-enhanced results 15 | - **Bing Web Grounding Tool** - Search the web with source citations 16 | 17 | 2. **Direct Azure AI Search Implementation** - Connects directly to Azure AI Search with three methods: 18 | - **Keyword Search** - Exact lexical matches 19 | - **Vector Search** - Semantic similarity using embeddings 20 | - **Hybrid Search** - Combination of keyword and vector searches 21 | 22 | --- 23 | 24 | ## Features 25 | 26 | - **AI-Enhanced Search** - Azure AI Agent Service optimizes search results with intelligent processing 27 | - **Multiple Data Sources** - Search both your private documents and the public web 28 | - **Source Citations** - Web search results include citations to original sources 29 | - **Flexible Implementation** - Choose between Azure AI Agent Service or direct Azure AI Search integration 30 | - **Seamless Claude Integration** - All search capabilities accessible through Claude Desktop's interface 31 | - **Customizable** - Easy to extend or modify search behavior 32 | 33 | --- 34 | 35 | ## Quick Links 36 | 37 | - [Get Started with Azure AI Search](https://learn.microsoft.com/en-us/azure/search/search-get-started-portal) 38 | - [Azure AI Agent Service Quickstart](https://learn.microsoft.com/en-us/azure/ai-studio/how-to/agent-quickstart) 39 | 40 | --- 41 | 42 | ## Requirements 43 | 44 | - **Python:** Version 3.10 or higher 45 | - **Claude Desktop:** Latest version 46 | - **Azure Resources:** 47 | - Azure AI Search service with an index containing vectorized text data 48 | - For Agent Service: Azure AI Project with Azure AI Search and Bing connections 49 | - **Operating System:** Windows or macOS (instructions provided for Windows, but adaptable) 50 | 51 | --- 52 | 53 | ## Azure AI Agent Service Implementation (Recommended) 54 | 55 | ### Setup Guide 56 | 57 | 1. **Project Directory:** 58 | 59 | ```bash 60 | mkdir mcp-server-azure-ai-search 61 | cd mcp-server-azure-ai-search 62 | ``` 63 | 64 | 2. **Create a `.env` File:** 65 | 66 | ```bash 67 | echo "PROJECT_CONNECTION_STRING=your-project-connection-string" > .env 68 | echo "MODEL_DEPLOYMENT_NAME=your-model-deployment-name" >> .env 69 | echo "AI_SEARCH_CONNECTION_NAME=your-search-connection-name" >> .env 70 | echo "BING_CONNECTION_NAME=your-bing-connection-name" >> .env 71 | echo "AI_SEARCH_INDEX_NAME=your-index-name" >> .env 72 | ``` 73 | 74 | 3. **Set Up Virtual Environment:** 75 | 76 | ```bash 77 | uv venv 78 | .venv\Scripts\activate 79 | uv pip install "mcp[cli]" azure-identity python-dotenv azure-ai-projects 80 | ``` 81 | 82 | 4. **Use the `azure_ai_agent_service_server.py` script** for integration with Azure AI Agent Service. 83 | 84 | ### Azure AI Agent Service Setup 85 | 86 | Before using the implementation, you need to: 87 | 88 | 1. **Create an Azure AI Project:** 89 | - Go to the Azure Portal and create a new Azure AI Project 90 | - Note the project connection string and model deployment name 91 | 92 | 2. **Create an Azure AI Search Connection:** 93 | - In your Azure AI Project, add a connection to your Azure AI Search service 94 | - Note the connection name and index name 95 | 96 | 3. **Create a Bing Web Search Connection:** 97 | - In your Azure AI Project, add a connection to Bing Search service 98 | - Note the connection name 99 | 100 | 4. **Authenticate with Azure:** 101 | ```bash 102 | az login 103 | ``` 104 | 105 | ### Configuring Claude Desktop 106 | 107 | ```json 108 | { 109 | "mcpServers": { 110 | "azure-ai-agent": { 111 | "command": "C:\\path\\to\\.venv\\Scripts\\python.exe", 112 | "args": ["C:\\path\\to\\azure_ai_agent_service_server.py"], 113 | "env": { 114 | "PROJECT_CONNECTION_STRING": "your-project-connection-string", 115 | "MODEL_DEPLOYMENT_NAME": "your-model-deployment-name", 116 | "AI_SEARCH_CONNECTION_NAME": "your-search-connection-name", 117 | "BING_CONNECTION_NAME": "your-bing-connection-name", 118 | "AI_SEARCH_INDEX_NAME": "your-index-name" 119 | } 120 | } 121 | } 122 | } 123 | ``` 124 | 125 | > **Note:** Replace path placeholders with your actual project paths. 126 | 127 | --- 128 | 129 | ## Direct Azure AI Search Implementation 130 | 131 | For those who prefer direct Azure AI Search integration without the Agent Service: 132 | 133 | 1. **Create a different `.env` File:** 134 | 135 | ```bash 136 | echo "AZURE_SEARCH_SERVICE_ENDPOINT=https://your-service-name.search.windows.net" > .env 137 | echo "AZURE_SEARCH_INDEX_NAME=your-index-name" >> .env 138 | echo "AZURE_SEARCH_API_KEY=your-api-key" >> .env 139 | ``` 140 | 141 | 2. **Install Dependencies:** 142 | 143 | ```bash 144 | uv pip install "mcp[cli]" azure-search-documents==11.5.2 azure-identity python-dotenv 145 | ``` 146 | 147 | 3. **Use the `azure_search_server.py` script** for direct integration with Azure AI Search. 148 | 149 | 4. **Configure Claude Desktop:** 150 | 151 | ```json 152 | { 153 | "mcpServers": { 154 | "azure-search": { 155 | "command": "C:\\path\\to\\.venv\\Scripts\\python.exe", 156 | "args": ["C:\\path\\to\\azure_search_server.py"], 157 | "env": { 158 | "AZURE_SEARCH_SERVICE_ENDPOINT": "https://your-service-name.search.windows.net", 159 | "AZURE_SEARCH_INDEX_NAME": "your-index-name", 160 | "AZURE_SEARCH_API_KEY": "your-api-key" 161 | } 162 | } 163 | } 164 | } 165 | ``` 166 | 167 | --- 168 | 169 | ## Testing the Server 170 | 171 | 1. **Restart Claude Desktop** to load the new configuration 172 | 2. Look for the MCP tools icon (hammer icon) in the bottom-right of the input field 173 | 3. Try queries such as: 174 | - "Search for information about AI in my Azure Search index" 175 | - "Search the web for the latest developments in LLMs" 176 | - "Find information about neural networks using hybrid search" 177 | 178 | --- 179 | 180 | ## Troubleshooting 181 | 182 | - **Server Not Appearing:** 183 | - Check Claude Desktop logs (located at `%APPDATA%\Claude\logs\mcp*.log` on Windows) 184 | - Verify file paths and environment variables in the configuration 185 | - Test running the server directly: `python azure_ai_agent_service_server.py` or `uv run python azure_ai_agent_service_server.py` 186 | 187 | - **Azure AI Agent Service Issues:** 188 | - Ensure your Azure AI Project is correctly configured 189 | - Verify that connections exist and are properly configured 190 | - Check your Azure authentication status 191 | 192 | --- 193 | 194 | ## Customizing Your Server 195 | 196 | - **Modify Tool Instructions:** Adjust the instructions provided to each agent to change how they process queries 197 | - **Add New Tools:** Use the `@mcp.tool()` decorator to integrate additional tools 198 | - **Customize Response Formatting:** Edit how responses are formatted and returned to Claude Desktop 199 | - **Adjust Web Search Parameters:** Modify the web search tool to focus on specific domains 200 | 201 | --- 202 | 203 | ## License 204 | 205 | This project is licensed under the MIT License. -------------------------------------------------------------------------------- /azure_ai_agent_service_server.py: -------------------------------------------------------------------------------- 1 | """Azure AI Agent Service MCP Server for Claude Desktop using Azure AI Search and Bing Web Grounding Tools.""" 2 | 3 | import os 4 | import sys 5 | import asyncio 6 | from dotenv import load_dotenv 7 | from mcp.server.fastmcp import FastMCP 8 | 9 | # Import Azure AI Agent Service modules 10 | from azure.ai.projects import AIProjectClient 11 | from azure.ai.projects.models import AzureAISearchTool, BingGroundingTool, MessageRole 12 | from azure.identity import DefaultAzureCredential 13 | 14 | # Add startup message 15 | print("Starting Azure AI Agent Service MCP Server...", file=sys.stderr) 16 | 17 | # Load environment variables 18 | load_dotenv() 19 | print("Environment variables loaded", file=sys.stderr) 20 | 21 | # Create MCP server 22 | mcp = FastMCP( 23 | "azure-ai-agent", 24 | description="MCP server for Azure AI Agent Service integration with AzureAISearch and Bing Web Grounding tools", 25 | dependencies=[ 26 | "azure-identity", 27 | "python-dotenv", 28 | "azure-ai-projects" 29 | ] 30 | ) 31 | print("MCP server instance created", file=sys.stderr) 32 | 33 | class AzureAIAgentClient: 34 | """Client for Azure AI Agent Service with Azure AI Search and Bing Web Grounding tools.""" 35 | 36 | def __init__(self): 37 | """Initialize Azure AI Agent Service client with credentials from environment variables.""" 38 | print("Initializing Azure AI Agent client...", file=sys.stderr) 39 | 40 | # Load environment variables 41 | self.project_connection_string = os.getenv("PROJECT_CONNECTION_STRING") 42 | self.model_deployment_name = os.getenv("MODEL_DEPLOYMENT_NAME") 43 | self.search_connection_name = os.getenv("AI_SEARCH_CONNECTION_NAME") 44 | self.bing_connection_name = os.getenv("BING_CONNECTION_NAME") 45 | self.index_name = os.getenv("AI_SEARCH_INDEX_NAME") 46 | 47 | # Validate environment variables 48 | required_vars = { 49 | "PROJECT_CONNECTION_STRING": self.project_connection_string, 50 | "MODEL_DEPLOYMENT_NAME": self.model_deployment_name, 51 | "AI_SEARCH_CONNECTION_NAME": self.search_connection_name, 52 | "BING_CONNECTION_NAME": self.bing_connection_name, 53 | "AI_SEARCH_INDEX_NAME": self.index_name 54 | } 55 | 56 | missing = [k for k, v in required_vars.items() if not v] 57 | if missing: 58 | error_msg = f"Missing environment variables: {', '.join(missing)}" 59 | print(f"Error: {error_msg}", file=sys.stderr) 60 | raise ValueError(error_msg) 61 | 62 | # Initialize AIProjectClient 63 | try: 64 | self.client = AIProjectClient.from_connection_string( 65 | credential=DefaultAzureCredential(), 66 | conn_str=self.project_connection_string 67 | ) 68 | print("AIProjectClient initialized successfully", file=sys.stderr) 69 | except Exception as e: 70 | print(f"Error initializing AIProjectClient: {str(e)}", file=sys.stderr) 71 | raise 72 | 73 | print(f"Azure AI Agent client initialized for AI Search connection: {self.search_connection_name}, Bing connection: {self.bing_connection_name}", file=sys.stderr) 74 | 75 | def search_index(self, query, top=5): 76 | """ 77 | Perform a search using Azure AI Search Tool (default: best/hybrid mode). 78 | 79 | Args: 80 | query: The search query text 81 | top: Maximum number of results to return 82 | 83 | Returns: 84 | Formatted search results 85 | """ 86 | print(f"Performing AI Search for: {query}", file=sys.stderr) 87 | 88 | try: 89 | # Get Azure AI Search connection 90 | search_connection = self.client.connections.get(connection_name=self.search_connection_name) 91 | if not search_connection: 92 | raise ValueError(f"Connection '{self.search_connection_name}' not found") 93 | 94 | # Create search tool 95 | search_tool = AzureAISearchTool( 96 | index_connection_id=search_connection.id, 97 | index_name=self.index_name 98 | ) 99 | 100 | # Create agent with the search tool 101 | agent = self.client.agents.create_agent( 102 | model=self.model_deployment_name, 103 | name="search-agent", 104 | instructions=f"You are an Azure AI Search expert. Use the Azure AI Search Tool to find the most relevant information for: '{query}'. Return only the top {top} most relevant results. For each result, provide a title, content excerpt, and relevance score if available. Format your response as Markdown with each result clearly separated.", 105 | tools=search_tool.definitions, 106 | tool_resources=search_tool.resources, 107 | headers={"x-ms-enable-preview": "true"} 108 | ) 109 | 110 | # Create thread for communication 111 | thread = self.client.agents.create_thread() 112 | 113 | # Create message to thread 114 | self.client.agents.create_message( 115 | thread_id=thread.id, 116 | role=MessageRole.USER, 117 | content=query 118 | ) 119 | 120 | # Process the run 121 | run = self.client.agents.create_and_process_run( 122 | thread_id=thread.id, 123 | agent_id=agent.id 124 | ) 125 | 126 | if run.status == "failed": 127 | print(f"Run failed: {run.last_error}", file=sys.stderr) 128 | return f"Search failed: {run.last_error}" 129 | 130 | # Get the agent's response 131 | response_message = self.client.agents.list_messages(thread_id=thread.id).get_last_message_by_role( 132 | MessageRole.AGENT 133 | ) 134 | 135 | result = "" 136 | if response_message: 137 | for text_message in response_message.text_messages: 138 | result += text_message.text.value + "\n" 139 | 140 | # Include any citations 141 | for annotation in response_message.url_citation_annotations: 142 | result += f"\nCitation: [{annotation.url_citation.title}]({annotation.url_citation.url})\n" 143 | 144 | # Clean up resources 145 | self.client.agents.delete_agent(agent.id) 146 | 147 | return result 148 | 149 | except Exception as e: 150 | print(f"Error during search: {str(e)}", file=sys.stderr) 151 | raise 152 | 153 | def web_search(self, query): 154 | """ 155 | Perform a web search using Bing Web Grounding Tool. 156 | 157 | Args: 158 | query: The search query text 159 | 160 | Returns: 161 | Formatted search results from the web 162 | """ 163 | print(f"Performing Bing Web search for: {query}", file=sys.stderr) 164 | 165 | try: 166 | # Get Bing connection 167 | bing_connection = self.client.connections.get(connection_name=self.bing_connection_name) 168 | if not bing_connection: 169 | raise ValueError(f"Connection '{self.bing_connection_name}' not found") 170 | 171 | # Initialize Bing Web Grounding Tool 172 | bing_tool = BingGroundingTool(connection_id=bing_connection.id) 173 | 174 | # Create agent with the Bing tool 175 | agent = self.client.agents.create_agent( 176 | model=self.model_deployment_name, 177 | name="web-search-agent", 178 | instructions=f"You are a helpful web search assistant. Use the Bing Web Grounding Tool to find the most current and accurate information for: '{query}'. Provide a comprehensive answer with citations to sources. Format your response as Markdown.", 179 | tools=bing_tool.definitions, 180 | headers={"x-ms-enable-preview": "true"} 181 | ) 182 | 183 | # Create thread for communication 184 | thread = self.client.agents.create_thread() 185 | 186 | # Create message to thread 187 | self.client.agents.create_message( 188 | thread_id=thread.id, 189 | role=MessageRole.USER, 190 | content=query 191 | ) 192 | 193 | # Process the run 194 | run = self.client.agents.create_and_process_run( 195 | thread_id=thread.id, 196 | agent_id=agent.id 197 | ) 198 | 199 | if run.status == "failed": 200 | print(f"Run failed: {run.last_error}", file=sys.stderr) 201 | return f"Web search failed: {run.last_error}" 202 | 203 | # Get the agent's response 204 | response_message = self.client.agents.list_messages(thread_id=thread.id).get_last_message_by_role( 205 | MessageRole.AGENT 206 | ) 207 | 208 | result = "" 209 | if response_message: 210 | for text_message in response_message.text_messages: 211 | result += text_message.text.value + "\n" 212 | 213 | # Include any citations 214 | for annotation in response_message.url_citation_annotations: 215 | result += f"\nCitation: [{annotation.url_citation.title}]({annotation.url_citation.url})\n" 216 | 217 | # Clean up resources 218 | self.client.agents.delete_agent(agent.id) 219 | 220 | return result 221 | 222 | except Exception as e: 223 | print(f"Error during web search: {str(e)}", file=sys.stderr) 224 | raise 225 | 226 | # Initialize Azure AI Agent client 227 | try: 228 | print("Starting initialization of agent client...", file=sys.stderr) 229 | agent_client = AzureAIAgentClient() 230 | print("Agent client initialized successfully", file=sys.stderr) 231 | except Exception as e: 232 | print(f"Error initializing agent client: {str(e)}", file=sys.stderr) 233 | # Don't exit - we'll handle errors in the tool functions 234 | agent_client = None 235 | 236 | @mcp.tool() 237 | def search_index(query: str, top: int = 5) -> str: 238 | """ 239 | Search your Azure AI Search index using the optimal retrieval method. 240 | 241 | Args: 242 | query: The search query text 243 | top: Maximum number of results to return (default: 5) 244 | 245 | Returns: 246 | Formatted search results from your indexed documents 247 | """ 248 | print(f"Tool called: search_index({query}, {top})", file=sys.stderr) 249 | if agent_client is None: 250 | return "Error: Azure AI Agent client is not initialized. Check server logs for details." 251 | 252 | try: 253 | results = agent_client.search_index(query, top) 254 | return f"## Azure AI Search Results\n\n{results}" 255 | except Exception as e: 256 | error_msg = f"Error performing index search: {str(e)}" 257 | print(error_msg, file=sys.stderr) 258 | return error_msg 259 | 260 | @mcp.tool() 261 | def web_search(query: str) -> str: 262 | """ 263 | Search the web using Bing Web Grounding to find the most current information. 264 | 265 | Args: 266 | query: The search query text 267 | 268 | Returns: 269 | Formatted search results from the web with citations 270 | """ 271 | print(f"Tool called: web_search({query})", file=sys.stderr) 272 | if agent_client is None: 273 | return "Error: Azure AI Agent client is not initialized. Check server logs for details." 274 | 275 | try: 276 | results = agent_client.web_search(query) 277 | return f"## Bing Web Search Results\n\n{results}" 278 | except Exception as e: 279 | error_msg = f"Error performing web search: {str(e)}" 280 | print(error_msg, file=sys.stderr) 281 | return error_msg 282 | 283 | if __name__ == "__main__": 284 | # Run the server with stdio transport (default) 285 | print("Starting MCP server run...", file=sys.stderr) 286 | mcp.run() -------------------------------------------------------------------------------- /azure_search_server.py: -------------------------------------------------------------------------------- 1 | """Azure AI Search MCP Server for Claude Desktop.""" 2 | 3 | import os 4 | import sys 5 | from dotenv import load_dotenv 6 | from azure.core.credentials import AzureKeyCredential 7 | from azure.search.documents import SearchClient 8 | from azure.search.documents.models import VectorizableTextQuery 9 | from mcp.server.fastmcp import FastMCP 10 | 11 | # Add startup message 12 | print("Starting Azure AI Search MCP Server...", file=sys.stderr) 13 | 14 | # Load environment variables 15 | load_dotenv() 16 | print("Environment variables loaded", file=sys.stderr) 17 | 18 | # Create MCP server 19 | mcp = FastMCP( 20 | "azure-search", 21 | description="MCP server for Azure AI Search integration", 22 | dependencies=["azure-search-documents==11.5.2", "azure-identity", "python-dotenv"] 23 | ) 24 | print("MCP server instance created", file=sys.stderr) 25 | 26 | class AzureSearchClient: 27 | """Client for Azure AI Search service.""" 28 | 29 | def __init__(self): 30 | """Initialize Azure Search client with credentials from environment variables.""" 31 | print("Initializing Azure Search client...", file=sys.stderr) 32 | # Load environment variables 33 | self.endpoint = os.getenv("AZURE_SEARCH_SERVICE_ENDPOINT") 34 | self.index_name = os.getenv("AZURE_SEARCH_INDEX_NAME") # Modified to use AZURE_SEARCH_INDEX 35 | api_key = os.getenv("AZURE_SEARCH_API_KEY") 36 | 37 | # Validate environment variables 38 | if not all([self.endpoint, self.index_name, api_key]): 39 | missing = [] 40 | if not self.endpoint: 41 | missing.append("AZURE_SEARCH_SERVICE_ENDPOINT") 42 | if not self.index_name: 43 | missing.append("AZURE_SEARCH_INDEX") 44 | if not api_key: 45 | missing.append("AZURE_SEARCH_API_KEY") 46 | error_msg = f"Missing environment variables: {', '.join(missing)}" 47 | print(f"Error: {error_msg}", file=sys.stderr) 48 | raise ValueError(error_msg) 49 | 50 | # Initialize the search client 51 | print(f"Connecting to Azure AI Search at {self.endpoint}", file=sys.stderr) 52 | self.credential = AzureKeyCredential(api_key) 53 | self.search_client = SearchClient( 54 | endpoint=self.endpoint, 55 | index_name=self.index_name, 56 | credential=self.credential 57 | ) 58 | print(f"Azure Search client initialized for index: {self.index_name}", file=sys.stderr) 59 | 60 | def keyword_search(self, query, top=5): 61 | """Perform keyword search on the index.""" 62 | print(f"Performing keyword search for: {query}", file=sys.stderr) 63 | results = self.search_client.search( 64 | search_text=query, 65 | top=top, 66 | select=["title", "chunk"] 67 | ) 68 | return self._format_results(results) 69 | 70 | def vector_search(self, query, top=5, vector_field="text_vector"): 71 | """Perform vector search on the index.""" 72 | print(f"Performing vector search for: {query}", file=sys.stderr) 73 | results = self.search_client.search( 74 | vector_queries=[ 75 | VectorizableTextQuery( 76 | text=query, 77 | k_nearest_neighbors=50, 78 | fields=vector_field 79 | ) 80 | ], 81 | top=top, 82 | select=["title", "chunk"] 83 | ) 84 | return self._format_results(results) 85 | 86 | def hybrid_search(self, query, top=5, vector_field="text_vector"): 87 | """Perform hybrid search (keyword + vector) on the index.""" 88 | print(f"Performing hybrid search for: {query}", file=sys.stderr) 89 | results = self.search_client.search( 90 | search_text=query, 91 | vector_queries=[ 92 | VectorizableTextQuery( 93 | text=query, 94 | k_nearest_neighbors=50, 95 | fields=vector_field 96 | ) 97 | ], 98 | top=top, 99 | select=["title", "chunk"] 100 | ) 101 | return self._format_results(results) 102 | 103 | def _format_results(self, results): 104 | """Format search results for better readability.""" 105 | formatted_results = [] 106 | for result in results: 107 | item = { 108 | "title": result.get("title", "Unknown"), 109 | "content": result.get("chunk", "")[:1000], # Limit content length 110 | "score": result.get("@search.score", 0) 111 | } 112 | formatted_results.append(item) 113 | 114 | print(f"Formatted {len(formatted_results)} search results", file=sys.stderr) 115 | return formatted_results 116 | 117 | # Initialize Azure Search client 118 | try: 119 | print("Starting initialization of search client...", file=sys.stderr) 120 | search_client = AzureSearchClient() 121 | print("Search client initialized successfully", file=sys.stderr) 122 | except Exception as e: 123 | print(f"Error initializing search client: {str(e)}", file=sys.stderr) 124 | # Don't exit - we'll handle errors in the tool functions 125 | search_client = None 126 | 127 | def _format_results_as_markdown(results, search_type): 128 | """Format search results as markdown for better readability.""" 129 | if not results: 130 | return f"No results found for your query using {search_type}." 131 | 132 | markdown = f"## {search_type} Results\n\n" 133 | 134 | for i, result in enumerate(results, 1): 135 | markdown += f"### {i}. {result['title']}\n" 136 | markdown += f"Score: {result['score']:.2f}\n\n" 137 | markdown += f"{result['content']}\n\n" 138 | markdown += "---\n\n" 139 | 140 | return markdown 141 | 142 | @mcp.tool() 143 | def keyword_search(query: str, top: int = 5) -> str: 144 | """ 145 | Perform a keyword-based search on the Azure AI Search index. 146 | 147 | Args: 148 | query: The search query text 149 | top: Maximum number of results to return (default: 5) 150 | 151 | Returns: 152 | Formatted search results 153 | """ 154 | print(f"Tool called: keyword_search({query}, {top})", file=sys.stderr) 155 | if search_client is None: 156 | return "Error: Azure Search client is not initialized. Check server logs for details." 157 | 158 | try: 159 | results = search_client.keyword_search(query, top) 160 | return _format_results_as_markdown(results, "Keyword Search") 161 | except Exception as e: 162 | error_msg = f"Error performing keyword search: {str(e)}" 163 | print(error_msg, file=sys.stderr) 164 | return error_msg 165 | 166 | @mcp.tool() 167 | def vector_search(query: str, top: int = 5) -> str: 168 | """ 169 | Perform a vector similarity search on the Azure AI Search index. 170 | 171 | Args: 172 | query: The search query text 173 | top: Maximum number of results to return (default: 5) 174 | 175 | Returns: 176 | Formatted search results 177 | """ 178 | print(f"Tool called: vector_search({query}, {top})", file=sys.stderr) 179 | if search_client is None: 180 | return "Error: Azure Search client is not initialized. Check server logs for details." 181 | 182 | try: 183 | results = search_client.vector_search(query, top) 184 | return _format_results_as_markdown(results, "Vector Search") 185 | except Exception as e: 186 | error_msg = f"Error performing vector search: {str(e)}" 187 | print(error_msg, file=sys.stderr) 188 | return error_msg 189 | 190 | @mcp.tool() 191 | def hybrid_search(query: str, top: int = 5) -> str: 192 | """ 193 | Perform a hybrid search (keyword + vector) on the Azure AI Search index. 194 | 195 | Args: 196 | query: The search query text 197 | top: Maximum number of results to return (default: 5) 198 | 199 | Returns: 200 | Formatted search results 201 | """ 202 | print(f"Tool called: hybrid_search({query}, {top})", file=sys.stderr) 203 | if search_client is None: 204 | return "Error: Azure Search client is not initialized. Check server logs for details." 205 | 206 | try: 207 | results = search_client.hybrid_search(query, top) 208 | return _format_results_as_markdown(results, "Hybrid Search") 209 | except Exception as e: 210 | error_msg = f"Error performing hybrid search: {str(e)}" 211 | print(error_msg, file=sys.stderr) 212 | return error_msg 213 | 214 | if __name__ == "__main__": 215 | # Run the server with stdio transport (default) 216 | print("Starting MCP server run...", file=sys.stderr) 217 | mcp.run() -------------------------------------------------------------------------------- /images/demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/farzad528/mcp-server-azure-ai-agents/c9458ac3301ea5a745295264e3fd353bb30555d7/images/demo.gif -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | aiofiles==23.2.1 2 | aiohappyeyeballs==2.6.1 3 | aiohttp==3.11.13 4 | aioice==0.9.0 5 | aiortc==1.10.1 6 | aiosignal==1.3.2 7 | annotated-types==0.7.0 8 | anyio==4.8.0 9 | argon2-cffi==23.1.0 10 | argon2-cffi-bindings==21.2.0 11 | arrow==1.3.0 12 | asttokens==3.0.0 13 | async-lru==2.0.4 14 | asyncer==0.0.7 15 | attrs==25.1.0 16 | av==13.1.0 17 | azure-ai-inference==1.0.0b9 18 | azure-ai-projects==1.0.0b7 19 | azure-common==1.1.28 20 | azure-core==1.32.0 21 | azure-core-tracing-opentelemetry==1.0.0b11 22 | azure-cosmos==4.9.0 23 | azure-identity==1.20.0 24 | azure-mgmt-core==1.5.0 25 | azure-mgmt-resource==23.2.0 26 | azure-search-documents==11.6.0b10 27 | -e git+https://github.com/farzad528/mcp-server-azure-ai-search.git@7d4ce398bf3f1ef98e743d4faf5e7942bd2eb305#egg=azure_search_mcp 28 | babel==2.17.0 29 | beautifulsoup4==4.13.3 30 | bidict==0.23.1 31 | bleach==6.2.0 32 | build==1.2.2.post1 33 | CacheControl==0.14.2 34 | certifi==2025.1.31 35 | cffi==1.17.1 36 | cfgv==3.4.0 37 | chainlit==2.2.1 38 | chardet==5.2.0 39 | charset-normalizer==3.4.1 40 | chevron==0.14.0 41 | cleo==2.1.0 42 | click==8.1.8 43 | cloudevents==1.11.0 44 | colorama==0.4.6 45 | comm==0.2.2 46 | crashtest==0.4.1 47 | cryptography==44.0.1 48 | dataclasses-json==0.6.7 49 | debugpy==1.8.12 50 | decorator==5.2.1 51 | defusedxml==0.7.1 52 | Deprecated==1.2.18 53 | deprecation==2.1.0 54 | distlib==0.3.9 55 | distro==1.9.0 56 | dnspython==2.7.0 57 | dulwich==0.22.8 58 | executing==2.2.0 59 | fastapi==0.115.8 60 | fastjsonschema==2.21.1 61 | filelock==3.17.0 62 | filetype==1.2.0 63 | findpython==0.6.3 64 | fqdn==1.5.1 65 | frozenlist==1.5.0 66 | google-crc32c==1.6.0 67 | googleapis-common-protos==1.68.0 68 | grpcio==1.70.0 69 | h11==0.14.0 70 | httpcore==1.0.7 71 | httpx==0.28.1 72 | httpx-sse==0.4.0 73 | identify==2.6.9 74 | idna==3.10 75 | ifaddr==0.2.0 76 | importlib_metadata==8.5.0 77 | installer==0.7.0 78 | ipykernel==6.29.5 79 | ipython==8.32.0 80 | ipywidgets==8.1.5 81 | isodate==0.7.2 82 | isoduration==20.11.0 83 | jaraco.classes==3.4.0 84 | jaraco.context==6.0.1 85 | jaraco.functools==4.1.0 86 | jedi==0.19.2 87 | Jinja2==3.1.5 88 | jiter==0.8.2 89 | json5==0.10.0 90 | jsonpointer==3.0.0 91 | jsonschema==4.23.0 92 | jsonschema-path==0.3.4 93 | jsonschema-specifications==2024.10.1 94 | jupyter==1.1.1 95 | jupyter-console==6.6.3 96 | jupyter-events==0.12.0 97 | jupyter-lsp==2.2.5 98 | jupyter_client==8.6.3 99 | jupyter_core==5.7.2 100 | jupyter_server==2.15.0 101 | jupyter_server_terminals==0.5.3 102 | jupyterlab==4.3.5 103 | jupyterlab_pygments==0.3.0 104 | jupyterlab_server==2.27.3 105 | jupyterlab_widgets==3.0.13 106 | keyring==25.6.0 107 | Lazify==0.4.0 108 | lazy-object-proxy==1.10.0 109 | literalai==0.1.103 110 | markdown-it-py==3.0.0 111 | MarkupSafe==3.0.2 112 | marshmallow==3.26.1 113 | matplotlib-inline==0.1.7 114 | mcp==1.4.1 115 | mdurl==0.1.2 116 | mistune==3.1.2 117 | more-itertools==10.6.0 118 | msal==1.31.1 119 | msal-extensions==1.2.0 120 | msgpack==1.1.0 121 | multidict==6.1.0 122 | mypy-extensions==1.0.0 123 | nbclient==0.10.2 124 | nbconvert==7.16.6 125 | nbformat==5.10.4 126 | nest-asyncio==1.6.0 127 | nodeenv==1.9.1 128 | notebook==7.3.2 129 | notebook_shim==0.2.4 130 | numpy==2.2.3 131 | openai==1.63.2 132 | openapi-core==0.19.4 133 | openapi-schema-validator==0.6.3 134 | openapi-spec-validator==0.7.1 135 | opentelemetry-api==1.29.0 136 | opentelemetry-exporter-otlp==1.29.0 137 | opentelemetry-exporter-otlp-proto-common==1.29.0 138 | opentelemetry-exporter-otlp-proto-grpc==1.29.0 139 | opentelemetry-exporter-otlp-proto-http==1.29.0 140 | opentelemetry-instrumentation==0.50b0 141 | opentelemetry-proto==1.29.0 142 | opentelemetry-sdk==1.29.0 143 | opentelemetry-semantic-conventions==0.50b0 144 | overrides==7.7.0 145 | packaging==24.2 146 | pandocfilters==1.5.1 147 | parse==1.20.2 148 | parso==0.8.4 149 | pathable==0.4.4 150 | pbs-installer==2025.2.12 151 | pkginfo==1.12.1.2 152 | platformdirs==4.3.6 153 | poetry==2.1.1 154 | poetry-core==2.1.1 155 | portalocker==2.10.1 156 | prance==23.6.21.0 157 | pre_commit==4.1.0 158 | prometheus_client==0.21.1 159 | prompt_toolkit==3.0.50 160 | propcache==0.3.0 161 | protobuf==5.29.3 162 | psutil==7.0.0 163 | pure_eval==0.2.3 164 | pybars4==0.9.13 165 | pycparser==2.22 166 | pydantic==2.10.6 167 | pydantic-settings==2.8.1 168 | pydantic_core==2.27.2 169 | pyee==12.1.1 170 | Pygments==2.19.1 171 | PyJWT==2.10.1 172 | pylibsrtp==0.11.0 173 | PyMeta3==0.5.1 174 | pyOpenSSL==25.0.0 175 | pyproject_hooks==1.2.0 176 | python-dateutil==2.9.0.post0 177 | python-dotenv==1.0.1 178 | python-engineio==4.11.2 179 | python-json-logger==3.2.1 180 | python-multipart==0.0.18 181 | python-socketio==5.12.1 182 | pywin32==308 183 | pywin32-ctypes==0.2.3 184 | pywinpty==2.0.15 185 | PyYAML==6.0.2 186 | pyzmq==26.2.1 187 | RapidFuzz==3.12.2 188 | referencing==0.36.2 189 | requests==2.32.3 190 | requests-toolbelt==1.0.0 191 | rfc3339-validator==0.1.4 192 | rfc3986-validator==0.1.1 193 | rich==13.9.4 194 | rpds-py==0.23.1 195 | ruamel.yaml==0.18.10 196 | scipy==1.15.2 197 | semantic-kernel==1.24.1 198 | Send2Trash==1.8.3 199 | setuptools==75.8.0 200 | shellingham==1.5.4 201 | simple-websocket==1.1.0 202 | six==1.17.0 203 | sniffio==1.3.1 204 | soupsieve==2.6 205 | sse-starlette==2.2.1 206 | stack-data==0.6.3 207 | starlette==0.41.3 208 | syncer==2.0.3 209 | terminado==0.18.1 210 | tinycss2==1.4.0 211 | tomli==2.2.1 212 | tomlkit==0.13.2 213 | tornado==6.4.2 214 | tqdm==4.67.1 215 | traitlets==5.14.3 216 | trove-classifiers==2025.3.3.18 217 | typer==0.15.2 218 | types-python-dateutil==2.9.0.20241206 219 | typing-inspect==0.9.0 220 | typing_extensions==4.12.2 221 | uptrace==1.29.0 222 | uri-template==1.3.0 223 | urllib3==2.3.0 224 | uv==0.6.5 225 | uvicorn==0.34.0 226 | virtualenv==20.29.3 227 | watchfiles==0.20.0 228 | wcwidth==0.2.13 229 | webcolors==24.11.1 230 | webencodings==0.5.1 231 | websocket-client==1.8.0 232 | websockets==14.2 233 | Werkzeug==3.1.3 234 | widgetsnbextension==4.0.13 235 | wrapt==1.17.2 236 | wsproto==1.2.0 237 | yarl==1.18.3 238 | zipp==3.21.0 239 | zstandard==0.23.0 240 | --------------------------------------------------------------------------------