microsoft-agents-a365-tooling-extensions-azureaifoundry 0.2.1.dev2__tar.gz → 0.2.1.dev4__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (14) hide show
  1. {microsoft_agents_a365_tooling_extensions_azureaifoundry-0.2.1.dev2 → microsoft_agents_a365_tooling_extensions_azureaifoundry-0.2.1.dev4}/PKG-INFO +1 -1
  2. microsoft_agents_a365_tooling_extensions_azureaifoundry-0.2.1.dev4/microsoft_agents_a365/tooling/extensions/azureaifoundry/services/mcp_tool_registration_service.py +472 -0
  3. {microsoft_agents_a365_tooling_extensions_azureaifoundry-0.2.1.dev2 → microsoft_agents_a365_tooling_extensions_azureaifoundry-0.2.1.dev4}/microsoft_agents_a365_tooling_extensions_azureaifoundry.egg-info/PKG-INFO +1 -1
  4. microsoft_agents_a365_tooling_extensions_azureaifoundry-0.2.1.dev2/microsoft_agents_a365/tooling/extensions/azureaifoundry/services/mcp_tool_registration_service.py +0 -219
  5. {microsoft_agents_a365_tooling_extensions_azureaifoundry-0.2.1.dev2 → microsoft_agents_a365_tooling_extensions_azureaifoundry-0.2.1.dev4}/README.md +0 -0
  6. {microsoft_agents_a365_tooling_extensions_azureaifoundry-0.2.1.dev2 → microsoft_agents_a365_tooling_extensions_azureaifoundry-0.2.1.dev4}/microsoft_agents_a365/tooling/extensions/azureaifoundry/__init__.py +0 -0
  7. {microsoft_agents_a365_tooling_extensions_azureaifoundry-0.2.1.dev2 → microsoft_agents_a365_tooling_extensions_azureaifoundry-0.2.1.dev4}/microsoft_agents_a365/tooling/extensions/azureaifoundry/services/__init__.py +0 -0
  8. {microsoft_agents_a365_tooling_extensions_azureaifoundry-0.2.1.dev2 → microsoft_agents_a365_tooling_extensions_azureaifoundry-0.2.1.dev4}/microsoft_agents_a365_tooling_extensions_azureaifoundry.egg-info/SOURCES.txt +0 -0
  9. {microsoft_agents_a365_tooling_extensions_azureaifoundry-0.2.1.dev2 → microsoft_agents_a365_tooling_extensions_azureaifoundry-0.2.1.dev4}/microsoft_agents_a365_tooling_extensions_azureaifoundry.egg-info/dependency_links.txt +0 -0
  10. {microsoft_agents_a365_tooling_extensions_azureaifoundry-0.2.1.dev2 → microsoft_agents_a365_tooling_extensions_azureaifoundry-0.2.1.dev4}/microsoft_agents_a365_tooling_extensions_azureaifoundry.egg-info/requires.txt +0 -0
  11. {microsoft_agents_a365_tooling_extensions_azureaifoundry-0.2.1.dev2 → microsoft_agents_a365_tooling_extensions_azureaifoundry-0.2.1.dev4}/microsoft_agents_a365_tooling_extensions_azureaifoundry.egg-info/top_level.txt +0 -0
  12. {microsoft_agents_a365_tooling_extensions_azureaifoundry-0.2.1.dev2 → microsoft_agents_a365_tooling_extensions_azureaifoundry-0.2.1.dev4}/pyproject.toml +0 -0
  13. {microsoft_agents_a365_tooling_extensions_azureaifoundry-0.2.1.dev2 → microsoft_agents_a365_tooling_extensions_azureaifoundry-0.2.1.dev4}/setup.cfg +0 -0
  14. {microsoft_agents_a365_tooling_extensions_azureaifoundry-0.2.1.dev2 → microsoft_agents_a365_tooling_extensions_azureaifoundry-0.2.1.dev4}/setup.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: microsoft-agents-a365-tooling-extensions-azureaifoundry
3
- Version: 0.2.1.dev2
3
+ Version: 0.2.1.dev4
4
4
  Summary: Azure AI Foundry integration for Agent 365 Tooling SDK
5
5
  Author-email: Microsoft <support@microsoft.com>
6
6
  License: MIT
@@ -0,0 +1,472 @@
1
+ # Copyright (c) Microsoft Corporation.
2
+ # Licensed under the MIT License.
3
+
4
+ """
5
+ MCP Tool Registration Service implementation for Azure Foundry.
6
+
7
+ This module provides the concrete implementation of the MCP (Model Context Protocol)
8
+ tool registration service that integrates with Azure Foundry to add MCP tool
9
+ servers to agents.
10
+ """
11
+
12
+ # Standard library imports
13
+ import logging
14
+ from typing import List, Optional, Sequence, Tuple
15
+
16
+ # Third-party imports - Azure AI
17
+ from azure.ai.agents import AgentsClient
18
+ from azure.ai.agents.models import McpTool, ThreadMessage, ToolResources
19
+ from azure.ai.projects import AIProjectClient
20
+ from azure.identity import DefaultAzureCredential
21
+ from microsoft_agents.hosting.core import Authorization, TurnContext
22
+
23
+ from microsoft_agents_a365.runtime import OperationError, OperationResult
24
+ from microsoft_agents_a365.runtime.utility import Utility
25
+ from microsoft_agents_a365.tooling.models import ChatHistoryMessage, ToolOptions
26
+ from microsoft_agents_a365.tooling.services.mcp_tool_server_configuration_service import (
27
+ McpToolServerConfigurationService,
28
+ )
29
+ from microsoft_agents_a365.tooling.utils.constants import Constants
30
+ from microsoft_agents_a365.tooling.utils.utility import get_mcp_platform_authentication_scope
31
+
32
+
33
+ class McpToolRegistrationService:
34
+ """
35
+ Provides MCP tool registration services for Azure Foundry agents.
36
+
37
+ This service handles registration and management of MCP (Model Context Protocol)
38
+ tool servers with Azure Foundry agents using the Azure AI SDK. It provides
39
+ seamless integration between MCP servers and Azure Foundry's agent framework.
40
+
41
+ Features:
42
+ - Automatic MCP server discovery and configuration
43
+ - Azure identity integration with DefaultAzureCredential
44
+ - Tool definitions and resources management
45
+ - Support for both development (ToolingManifest.json) and production (gateway API) scenarios
46
+ - Comprehensive error handling and logging
47
+
48
+ Example:
49
+ >>> service = McpToolRegistrationService()
50
+ >>> service.add_tool_servers_to_agent(project_client, agent_id, token)
51
+ """
52
+
53
+ _orchestrator_name: str = "AzureAIFoundry"
54
+
55
+ def __init__(
56
+ self,
57
+ logger: Optional[logging.Logger] = None,
58
+ credential: Optional["DefaultAzureCredential"] = None,
59
+ ):
60
+ """
61
+ Initialize the MCP Tool Registration Service for Azure Foundry.
62
+
63
+ Args:
64
+ logger: Logger instance for logging operations.
65
+ credential: Azure credential for authentication. If None, DefaultAzureCredential will be used.
66
+ """
67
+ self._logger = logger or logging.getLogger(self.__class__.__name__)
68
+ self._credential = credential or DefaultAzureCredential()
69
+ self._mcp_server_configuration_service = McpToolServerConfigurationService(
70
+ logger=self._logger
71
+ )
72
+
73
+ # ============================================================================
74
+ # Public Methods - Main Entry Points
75
+ # ============================================================================
76
+
77
+ async def add_tool_servers_to_agent(
78
+ self,
79
+ project_client: "AIProjectClient",
80
+ auth: Authorization,
81
+ auth_handler_name: str,
82
+ context: TurnContext,
83
+ auth_token: Optional[str] = None,
84
+ ) -> None:
85
+ """
86
+ Adds MCP tool servers to an Azure Foundry agent.
87
+
88
+ Args:
89
+ project_client: The Azure Foundry AIProjectClient instance.
90
+ auth: Authorization handler for token exchange.
91
+ auth_handler_name: Name of the authorization handler.
92
+ context: Turn context for the current operation.
93
+ auth_token: Authentication token to access the MCP servers.
94
+
95
+ Raises:
96
+ ValueError: If project_client is None or required parameters are invalid.
97
+ Exception: If there's an error during MCP tool registration.
98
+ """
99
+ if project_client is None:
100
+ raise ValueError("project_client cannot be None")
101
+
102
+ if not auth_token:
103
+ scopes = get_mcp_platform_authentication_scope()
104
+ authToken = await auth.exchange_token(context, scopes, auth_handler_name)
105
+ auth_token = authToken.token
106
+
107
+ try:
108
+ agentic_app_id = Utility.resolve_agent_identity(context, auth_token)
109
+ # Get the tool definitions and resources using the async implementation
110
+ tool_definitions, tool_resources = await self._get_mcp_tool_definitions_and_resources(
111
+ agentic_app_id, auth_token or ""
112
+ )
113
+
114
+ # Update the agent with the tools
115
+ project_client.agents.update_agent(
116
+ agentic_app_id, tools=tool_definitions, tool_resources=tool_resources
117
+ )
118
+
119
+ self._logger.info(
120
+ f"Successfully configured {len(tool_definitions)} MCP tool servers for agent"
121
+ )
122
+
123
+ except Exception as ex:
124
+ self._logger.error(
125
+ f"Unhandled failure during MCP tool registration workflow for agent user {agentic_app_id}: {ex}"
126
+ )
127
+ raise
128
+
129
+ async def _get_mcp_tool_definitions_and_resources(
130
+ self, agentic_app_id: str, auth_token: str
131
+ ) -> Tuple[List[McpTool], Optional[ToolResources]]:
132
+ """
133
+ Internal method to get MCP tool definitions and resources.
134
+
135
+ This implements the core logic equivalent to the C# method of the same name.
136
+
137
+ Args:
138
+ agentic_app_id: Agentic App ID for the agent.
139
+ auth_token: Authentication token to access the MCP servers.
140
+
141
+ Returns:
142
+ Tuple containing tool definitions and resources.
143
+ """
144
+ if self._mcp_server_configuration_service is None:
145
+ self._logger.error("MCP server configuration service is not available")
146
+ return ([], None)
147
+
148
+ # Get MCP server configurations
149
+ options = ToolOptions(orchestrator_name=self._orchestrator_name)
150
+ try:
151
+ servers = await self._mcp_server_configuration_service.list_tool_servers(
152
+ agentic_app_id, auth_token, options
153
+ )
154
+ except Exception as ex:
155
+ self._logger.error(
156
+ f"Failed to list MCP tool servers for AgenticAppId={agentic_app_id}: {ex}"
157
+ )
158
+ return ([], None)
159
+
160
+ if len(servers) == 0:
161
+ self._logger.info(f"No MCP servers configured for AgenticAppId={agentic_app_id}")
162
+ return ([], None)
163
+
164
+ # Collections to build for the return value
165
+ tool_definitions: List[McpTool] = []
166
+ combined_tool_resources = ToolResources()
167
+
168
+ for server in servers:
169
+ # Validate server configuration
170
+ if not server.mcp_server_name or not server.mcp_server_unique_name:
171
+ self._logger.warning(
172
+ f"Skipping invalid MCP server config: Name='{server.mcp_server_name}', Url='{server.mcp_server_unique_name}'"
173
+ )
174
+ continue
175
+
176
+ # TODO: The Foundry SDK currently allows MCP label names without the "mcp_" prefix,
177
+ # which is unintended and has been identified as a bug.
178
+ # This change should be reverted once the official fix is availab
179
+ server_label = (
180
+ server.mcp_server_name[4:]
181
+ if server.mcp_server_name.lower().startswith("mcp_")
182
+ else server.mcp_server_name
183
+ )
184
+
185
+ # Use the URL from server (always populated by the configuration service)
186
+ server_url = server.url
187
+
188
+ # Create MCP tool using Azure Foundry SDK
189
+ mcp_tool = McpTool(server_label=server_label, server_url=server_url)
190
+
191
+ # Configure the tool
192
+ mcp_tool.set_approval_mode("never")
193
+
194
+ # Set up authorization header
195
+ if auth_token:
196
+ header_value = (
197
+ auth_token
198
+ if auth_token.lower().startswith(f"{Constants.Headers.BEARER_PREFIX.lower()} ")
199
+ else f"{Constants.Headers.BEARER_PREFIX} {auth_token}"
200
+ )
201
+ mcp_tool.update_headers(Constants.Headers.AUTHORIZATION, header_value)
202
+
203
+ mcp_tool.update_headers(
204
+ Constants.Headers.USER_AGENT, Utility.get_user_agent_header(self._orchestrator_name)
205
+ )
206
+
207
+ # Add to collections
208
+ tool_definitions.extend(mcp_tool.definitions)
209
+ if mcp_tool.resources and mcp_tool.resources.mcp:
210
+ if combined_tool_resources.mcp is None:
211
+ combined_tool_resources.mcp = []
212
+ combined_tool_resources.mcp.extend(mcp_tool.resources.mcp)
213
+
214
+ # Return None if no servers were processed successfully
215
+ if combined_tool_resources.mcp is None or len(combined_tool_resources.mcp) == 0:
216
+ combined_tool_resources = None
217
+
218
+ self._logger.info(
219
+ f"Processed {len(servers)} MCP servers, created {len(tool_definitions)} tool definitions"
220
+ )
221
+
222
+ return (tool_definitions, combined_tool_resources)
223
+
224
+ # ============================================================================
225
+ # Public Methods - Chat History API
226
+ # ============================================================================
227
+
228
+ async def send_chat_history_messages(
229
+ self,
230
+ turn_context: TurnContext,
231
+ messages: Sequence[ThreadMessage],
232
+ tool_options: Optional[ToolOptions] = None,
233
+ ) -> OperationResult:
234
+ """
235
+ Send Azure AI Foundry chat history messages to the MCP platform.
236
+
237
+ This method accepts a sequence of Azure AI Foundry ThreadMessage objects,
238
+ converts them to ChatHistoryMessage format, and sends them to the MCP
239
+ platform for real-time threat protection.
240
+
241
+ Args:
242
+ turn_context: TurnContext from the Agents SDK containing conversation info.
243
+ messages: Sequence of Azure AI Foundry ThreadMessage objects to send.
244
+ tool_options: Optional configuration for the request.
245
+
246
+ Returns:
247
+ OperationResult indicating success or failure.
248
+
249
+ Raises:
250
+ ValueError: If turn_context or messages is None.
251
+
252
+ Example:
253
+ >>> service = McpToolRegistrationService()
254
+ >>> messages = await agents_client.messages.list(thread_id=thread_id)
255
+ >>> result = await service.send_chat_history_messages(
256
+ ... turn_context, list(messages)
257
+ ... )
258
+ >>> if result.succeeded:
259
+ ... print("Chat history sent successfully")
260
+ """
261
+ # Input validation
262
+ if turn_context is None:
263
+ raise ValueError("turn_context cannot be None")
264
+ if messages is None:
265
+ raise ValueError("messages cannot be None")
266
+
267
+ self._logger.info(f"Sending {len(messages)} Azure AI Foundry messages as chat history")
268
+
269
+ # Set default options with orchestrator name
270
+ if tool_options is None:
271
+ tool_options = ToolOptions(orchestrator_name=self._orchestrator_name)
272
+ elif tool_options.orchestrator_name is None:
273
+ tool_options.orchestrator_name = self._orchestrator_name
274
+
275
+ try:
276
+ # Convert ThreadMessage objects to ChatHistoryMessage format
277
+ chat_history_messages = self._convert_thread_messages_to_chat_history(messages)
278
+
279
+ self._logger.debug(
280
+ f"Converted {len(chat_history_messages)} messages to ChatHistoryMessage format"
281
+ )
282
+
283
+ # Delegate to core service
284
+ result = await self._mcp_server_configuration_service.send_chat_history(
285
+ turn_context=turn_context,
286
+ chat_history_messages=chat_history_messages,
287
+ options=tool_options,
288
+ )
289
+
290
+ if result.succeeded:
291
+ self._logger.info(
292
+ f"Chat history sent successfully with {len(chat_history_messages)} messages"
293
+ )
294
+ else:
295
+ self._logger.error(f"Failed to send chat history: {result}")
296
+
297
+ return result
298
+
299
+ except ValueError:
300
+ # Re-raise validation errors from the core service
301
+ raise
302
+ except Exception as ex:
303
+ self._logger.error(f"Failed to send chat history messages: {ex}")
304
+ return OperationResult.failed(OperationError(ex))
305
+
306
+ async def send_chat_history(
307
+ self,
308
+ agents_client: AgentsClient,
309
+ thread_id: str,
310
+ turn_context: TurnContext,
311
+ tool_options: Optional[ToolOptions] = None,
312
+ ) -> OperationResult:
313
+ """
314
+ Retrieve and send chat history from Azure AI Foundry to the MCP platform.
315
+
316
+ This method retrieves messages from the Azure AI Foundry Agents API using
317
+ the provided client and thread ID, converts them to ChatHistoryMessage
318
+ format, and sends them to the MCP platform.
319
+
320
+ Args:
321
+ agents_client: The Azure AI Foundry AgentsClient instance.
322
+ thread_id: The thread ID containing the messages to send.
323
+ turn_context: TurnContext from the Agents SDK containing conversation info.
324
+ tool_options: Optional configuration for the request.
325
+
326
+ Returns:
327
+ OperationResult indicating success or failure.
328
+
329
+ Raises:
330
+ ValueError: If agents_client, thread_id, or turn_context is None/empty.
331
+
332
+ Example:
333
+ >>> from azure.ai.agents import AgentsClient
334
+ >>> from azure.identity import DefaultAzureCredential
335
+ >>>
336
+ >>> client = AgentsClient(endpoint, credential=DefaultAzureCredential())
337
+ >>> service = McpToolRegistrationService()
338
+ >>> result = await service.send_chat_history(
339
+ ... client, thread_id, turn_context
340
+ ... )
341
+ """
342
+ # Input validation
343
+ if agents_client is None:
344
+ raise ValueError("agents_client cannot be None")
345
+ if thread_id is None or not thread_id.strip():
346
+ raise ValueError("thread_id cannot be empty")
347
+ if turn_context is None:
348
+ raise ValueError("turn_context cannot be None")
349
+
350
+ try:
351
+ # Retrieve messages from the thread
352
+ messages: List[ThreadMessage] = []
353
+ async for message in agents_client.messages.list(thread_id=thread_id):
354
+ messages.append(message)
355
+
356
+ self._logger.info(f"Retrieved {len(messages)} messages from thread {thread_id}")
357
+
358
+ # Delegate to send_chat_history_messages
359
+ return await self.send_chat_history_messages(
360
+ turn_context=turn_context,
361
+ messages=messages,
362
+ tool_options=tool_options,
363
+ )
364
+
365
+ except ValueError:
366
+ # Re-raise validation errors
367
+ raise
368
+ except Exception as ex:
369
+ self._logger.error(f"Failed to send chat history from thread {thread_id}: {ex}")
370
+ return OperationResult.failed(OperationError(ex))
371
+
372
+ # ============================================================================
373
+ # Private Methods - Message Conversion Helpers
374
+ # ============================================================================
375
+
376
+ def _convert_thread_messages_to_chat_history(
377
+ self,
378
+ messages: Sequence[ThreadMessage],
379
+ ) -> List[ChatHistoryMessage]:
380
+ """
381
+ Convert Azure AI Foundry ThreadMessage objects to ChatHistoryMessage format.
382
+
383
+ This internal helper method transforms Azure AI Foundry's native ThreadMessage
384
+ objects into the ChatHistoryMessage format expected by the MCP platform's
385
+ real-time threat protection endpoint.
386
+
387
+ Args:
388
+ messages: Sequence of ThreadMessage objects to convert.
389
+
390
+ Returns:
391
+ List of ChatHistoryMessage objects ready for the MCP platform.
392
+
393
+ Note:
394
+ - Messages with None id, None role, or empty content are filtered out
395
+ - Role is extracted via the .value property of the MessageRole enum
396
+ - Timestamp is taken from message.created_at
397
+ """
398
+ history_messages: List[ChatHistoryMessage] = []
399
+
400
+ for message in messages:
401
+ # Skip None messages
402
+ if message is None:
403
+ self._logger.warning("Skipping null message")
404
+ continue
405
+
406
+ # Skip messages with None id
407
+ if message.id is None:
408
+ self._logger.warning("Skipping message with null ID")
409
+ continue
410
+
411
+ # Skip messages with None role
412
+ if message.role is None:
413
+ self._logger.warning(f"Skipping message with null role (ID: {message.id})")
414
+ continue
415
+
416
+ # Extract content from message
417
+ content = self._extract_content_from_message(message)
418
+
419
+ # Skip messages with empty content
420
+ if not content or not content.strip():
421
+ self._logger.warning(f"Skipping message {message.id} with empty content")
422
+ continue
423
+
424
+ # Convert role enum to lowercase string
425
+ role_value = message.role.value if hasattr(message.role, "value") else str(message.role)
426
+ role = role_value.lower()
427
+
428
+ # Create ChatHistoryMessage
429
+ history_message = ChatHistoryMessage(
430
+ id=message.id,
431
+ role=role,
432
+ content=content,
433
+ timestamp=message.created_at,
434
+ )
435
+ history_messages.append(history_message)
436
+
437
+ self._logger.debug(
438
+ f"Converted message {message.id} with role '{role}' to ChatHistoryMessage"
439
+ )
440
+
441
+ if len(history_messages) == 0 and len(messages) > 0:
442
+ self._logger.warning("All messages were filtered out during conversion")
443
+
444
+ return history_messages
445
+
446
+ def _extract_content_from_message(self, message: ThreadMessage) -> str:
447
+ """
448
+ Extract text content from a ThreadMessage's content items.
449
+
450
+ This method iterates through the message's content list and extracts
451
+ text from MessageTextContent items, concatenating them with spaces.
452
+
453
+ Args:
454
+ message: Azure AI Foundry ThreadMessage object.
455
+
456
+ Returns:
457
+ Concatenated text content as string, or empty string if no text found.
458
+ """
459
+ if message.content is None or len(message.content) == 0:
460
+ return ""
461
+
462
+ text_parts: List[str] = []
463
+
464
+ for content_item in message.content:
465
+ # Check for MessageTextContent by duck typing (has text attribute with value)
466
+ # This handles both real SDK types and mock objects in tests
467
+ if hasattr(content_item, "text") and content_item.text is not None:
468
+ text_value = getattr(content_item.text, "value", None)
469
+ if text_value is not None and text_value:
470
+ text_parts.append(text_value)
471
+
472
+ return " ".join(text_parts)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: microsoft-agents-a365-tooling-extensions-azureaifoundry
3
- Version: 0.2.1.dev2
3
+ Version: 0.2.1.dev4
4
4
  Summary: Azure AI Foundry integration for Agent 365 Tooling SDK
5
5
  Author-email: Microsoft <support@microsoft.com>
6
6
  License: MIT
@@ -1,219 +0,0 @@
1
- # Copyright (c) Microsoft Corporation.
2
- # Licensed under the MIT License.
3
-
4
- """
5
- MCP Tool Registration Service implementation for Azure Foundry.
6
-
7
- This module provides the concrete implementation of the MCP (Model Context Protocol)
8
- tool registration service that integrates with Azure Foundry to add MCP tool
9
- servers to agents.
10
- """
11
-
12
- # Standard library imports
13
- import logging
14
- from typing import Optional, List, Tuple
15
-
16
- # Third-party imports - Azure AI
17
- from azure.ai.projects import AIProjectClient
18
- from azure.identity import DefaultAzureCredential
19
- from azure.ai.agents.models import McpTool, ToolResources
20
- from microsoft_agents.hosting.core import Authorization, TurnContext
21
- from microsoft_agents_a365.runtime.utility import Utility
22
- from microsoft_agents_a365.tooling.services.mcp_tool_server_configuration_service import (
23
- McpToolServerConfigurationService,
24
- )
25
- from microsoft_agents_a365.tooling.models import ToolOptions
26
- from microsoft_agents_a365.tooling.utils.constants import Constants
27
- from microsoft_agents_a365.tooling.utils.utility import get_mcp_platform_authentication_scope
28
-
29
-
30
- class McpToolRegistrationService:
31
- """
32
- Provides MCP tool registration services for Azure Foundry agents.
33
-
34
- This service handles registration and management of MCP (Model Context Protocol)
35
- tool servers with Azure Foundry agents using the Azure AI SDK. It provides
36
- seamless integration between MCP servers and Azure Foundry's agent framework.
37
-
38
- Features:
39
- - Automatic MCP server discovery and configuration
40
- - Azure identity integration with DefaultAzureCredential
41
- - Tool definitions and resources management
42
- - Support for both development (ToolingManifest.json) and production (gateway API) scenarios
43
- - Comprehensive error handling and logging
44
-
45
- Example:
46
- >>> service = McpToolRegistrationService()
47
- >>> service.add_tool_servers_to_agent(project_client, agent_id, token)
48
- """
49
-
50
- _orchestrator_name: str = "AzureAIFoundry"
51
-
52
- def __init__(
53
- self,
54
- logger: Optional[logging.Logger] = None,
55
- credential: Optional["DefaultAzureCredential"] = None,
56
- ):
57
- """
58
- Initialize the MCP Tool Registration Service for Azure Foundry.
59
-
60
- Args:
61
- logger: Logger instance for logging operations.
62
- credential: Azure credential for authentication. If None, DefaultAzureCredential will be used.
63
- """
64
- self._logger = logger or logging.getLogger(self.__class__.__name__)
65
- self._credential = credential or DefaultAzureCredential()
66
- self._mcp_server_configuration_service = McpToolServerConfigurationService(
67
- logger=self._logger
68
- )
69
-
70
- # ============================================================================
71
- # Public Methods - Main Entry Points
72
- # ============================================================================
73
-
74
- async def add_tool_servers_to_agent(
75
- self,
76
- project_client: "AIProjectClient",
77
- auth: Authorization,
78
- auth_handler_name: str,
79
- context: TurnContext,
80
- auth_token: Optional[str] = None,
81
- ) -> None:
82
- """
83
- Adds MCP tool servers to an Azure Foundry agent.
84
-
85
- Args:
86
- project_client: The Azure Foundry AIProjectClient instance.
87
- auth: Authorization handler for token exchange.
88
- auth_handler_name: Name of the authorization handler.
89
- context: Turn context for the current operation.
90
- auth_token: Authentication token to access the MCP servers.
91
-
92
- Raises:
93
- ValueError: If project_client is None or required parameters are invalid.
94
- Exception: If there's an error during MCP tool registration.
95
- """
96
- if project_client is None:
97
- raise ValueError("project_client cannot be None")
98
-
99
- if not auth_token:
100
- scopes = get_mcp_platform_authentication_scope()
101
- authToken = await auth.exchange_token(context, scopes, auth_handler_name)
102
- auth_token = authToken.token
103
-
104
- try:
105
- agentic_app_id = Utility.resolve_agent_identity(context, auth_token)
106
- # Get the tool definitions and resources using the async implementation
107
- tool_definitions, tool_resources = await self._get_mcp_tool_definitions_and_resources(
108
- agentic_app_id, auth_token or ""
109
- )
110
-
111
- # Update the agent with the tools
112
- project_client.agents.update_agent(
113
- agentic_app_id, tools=tool_definitions, tool_resources=tool_resources
114
- )
115
-
116
- self._logger.info(
117
- f"Successfully configured {len(tool_definitions)} MCP tool servers for agent"
118
- )
119
-
120
- except Exception as ex:
121
- self._logger.error(
122
- f"Unhandled failure during MCP tool registration workflow for agent user {agentic_app_id}: {ex}"
123
- )
124
- raise
125
-
126
- async def _get_mcp_tool_definitions_and_resources(
127
- self, agentic_app_id: str, auth_token: str
128
- ) -> Tuple[List[McpTool], Optional[ToolResources]]:
129
- """
130
- Internal method to get MCP tool definitions and resources.
131
-
132
- This implements the core logic equivalent to the C# method of the same name.
133
-
134
- Args:
135
- agentic_app_id: Agentic App ID for the agent.
136
- auth_token: Authentication token to access the MCP servers.
137
-
138
- Returns:
139
- Tuple containing tool definitions and resources.
140
- """
141
- if self._mcp_server_configuration_service is None:
142
- self._logger.error("MCP server configuration service is not available")
143
- return ([], None)
144
-
145
- # Get MCP server configurations
146
- options = ToolOptions(orchestrator_name=self._orchestrator_name)
147
- try:
148
- servers = await self._mcp_server_configuration_service.list_tool_servers(
149
- agentic_app_id, auth_token, options
150
- )
151
- except Exception as ex:
152
- self._logger.error(
153
- f"Failed to list MCP tool servers for AgenticAppId={agentic_app_id}: {ex}"
154
- )
155
- return ([], None)
156
-
157
- if len(servers) == 0:
158
- self._logger.info(f"No MCP servers configured for AgenticAppId={agentic_app_id}")
159
- return ([], None)
160
-
161
- # Collections to build for the return value
162
- tool_definitions: List[McpTool] = []
163
- combined_tool_resources = ToolResources()
164
-
165
- for server in servers:
166
- # Validate server configuration
167
- if not server.mcp_server_name or not server.mcp_server_unique_name:
168
- self._logger.warning(
169
- f"Skipping invalid MCP server config: Name='{server.mcp_server_name}', Url='{server.mcp_server_unique_name}'"
170
- )
171
- continue
172
-
173
- # TODO: The Foundry SDK currently allows MCP label names without the "mcp_" prefix,
174
- # which is unintended and has been identified as a bug.
175
- # This change should be reverted once the official fix is availab
176
- server_label = (
177
- server.mcp_server_name[4:]
178
- if server.mcp_server_name.lower().startswith("mcp_")
179
- else server.mcp_server_name
180
- )
181
-
182
- # Use the URL from server (always populated by the configuration service)
183
- server_url = server.url
184
-
185
- # Create MCP tool using Azure Foundry SDK
186
- mcp_tool = McpTool(server_label=server_label, server_url=server_url)
187
-
188
- # Configure the tool
189
- mcp_tool.set_approval_mode("never")
190
-
191
- # Set up authorization header
192
- if auth_token:
193
- header_value = (
194
- auth_token
195
- if auth_token.lower().startswith(f"{Constants.Headers.BEARER_PREFIX.lower()} ")
196
- else f"{Constants.Headers.BEARER_PREFIX} {auth_token}"
197
- )
198
- mcp_tool.update_headers(Constants.Headers.AUTHORIZATION, header_value)
199
-
200
- mcp_tool.update_headers(
201
- Constants.Headers.USER_AGENT, Utility.get_user_agent_header(self._orchestrator_name)
202
- )
203
-
204
- # Add to collections
205
- tool_definitions.extend(mcp_tool.definitions)
206
- if mcp_tool.resources and mcp_tool.resources.mcp:
207
- if combined_tool_resources.mcp is None:
208
- combined_tool_resources.mcp = []
209
- combined_tool_resources.mcp.extend(mcp_tool.resources.mcp)
210
-
211
- # Return None if no servers were processed successfully
212
- if combined_tool_resources.mcp is None or len(combined_tool_resources.mcp) == 0:
213
- combined_tool_resources = None
214
-
215
- self._logger.info(
216
- f"Processed {len(servers)} MCP servers, created {len(tool_definitions)} tool definitions"
217
- )
218
-
219
- return (tool_definitions, combined_tool_resources)