microsoft-agents-a365-tooling-extensions-azureaifoundry 0.2.1.dev0__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.
- {microsoft_agents_a365_tooling_extensions_azureaifoundry-0.2.1.dev0 → microsoft_agents_a365_tooling_extensions_azureaifoundry-0.2.1.dev4}/PKG-INFO +1 -3
- microsoft_agents_a365_tooling_extensions_azureaifoundry-0.2.1.dev4/microsoft_agents_a365/tooling/extensions/azureaifoundry/services/mcp_tool_registration_service.py +472 -0
- {microsoft_agents_a365_tooling_extensions_azureaifoundry-0.2.1.dev0 → microsoft_agents_a365_tooling_extensions_azureaifoundry-0.2.1.dev4}/microsoft_agents_a365_tooling_extensions_azureaifoundry.egg-info/PKG-INFO +1 -3
- {microsoft_agents_a365_tooling_extensions_azureaifoundry-0.2.1.dev0 → microsoft_agents_a365_tooling_extensions_azureaifoundry-0.2.1.dev4}/microsoft_agents_a365_tooling_extensions_azureaifoundry.egg-info/SOURCES.txt +0 -3
- microsoft_agents_a365_tooling_extensions_azureaifoundry-0.2.1.dev0/microsoft_agents_a365/tooling/extensions/azureaifoundry/services/mcp_tool_registration_service.py +0 -219
- {microsoft_agents_a365_tooling_extensions_azureaifoundry-0.2.1.dev0 → microsoft_agents_a365_tooling_extensions_azureaifoundry-0.2.1.dev4}/README.md +0 -0
- {microsoft_agents_a365_tooling_extensions_azureaifoundry-0.2.1.dev0 → microsoft_agents_a365_tooling_extensions_azureaifoundry-0.2.1.dev4}/microsoft_agents_a365/tooling/extensions/azureaifoundry/__init__.py +0 -0
- {microsoft_agents_a365_tooling_extensions_azureaifoundry-0.2.1.dev0 → microsoft_agents_a365_tooling_extensions_azureaifoundry-0.2.1.dev4}/microsoft_agents_a365/tooling/extensions/azureaifoundry/services/__init__.py +0 -0
- {microsoft_agents_a365_tooling_extensions_azureaifoundry-0.2.1.dev0 → microsoft_agents_a365_tooling_extensions_azureaifoundry-0.2.1.dev4}/microsoft_agents_a365_tooling_extensions_azureaifoundry.egg-info/dependency_links.txt +0 -0
- {microsoft_agents_a365_tooling_extensions_azureaifoundry-0.2.1.dev0 → microsoft_agents_a365_tooling_extensions_azureaifoundry-0.2.1.dev4}/microsoft_agents_a365_tooling_extensions_azureaifoundry.egg-info/requires.txt +0 -0
- {microsoft_agents_a365_tooling_extensions_azureaifoundry-0.2.1.dev0 → microsoft_agents_a365_tooling_extensions_azureaifoundry-0.2.1.dev4}/microsoft_agents_a365_tooling_extensions_azureaifoundry.egg-info/top_level.txt +0 -0
- {microsoft_agents_a365_tooling_extensions_azureaifoundry-0.2.1.dev0 → microsoft_agents_a365_tooling_extensions_azureaifoundry-0.2.1.dev4}/pyproject.toml +0 -0
- {microsoft_agents_a365_tooling_extensions_azureaifoundry-0.2.1.dev0 → microsoft_agents_a365_tooling_extensions_azureaifoundry-0.2.1.dev4}/setup.cfg +0 -0
- {microsoft_agents_a365_tooling_extensions_azureaifoundry-0.2.1.dev0 → 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.
|
|
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
|
|
@@ -18,7 +18,6 @@ Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
|
18
18
|
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
19
19
|
Requires-Python: >=3.11
|
|
20
20
|
Description-Content-Type: text/markdown
|
|
21
|
-
License-File: LICENSE
|
|
22
21
|
Requires-Dist: microsoft-agents-a365-tooling>=0.0.0
|
|
23
22
|
Requires-Dist: azure-ai-projects>=2.0.0b1
|
|
24
23
|
Requires-Dist: azure-ai-agents>=1.0.0b251001
|
|
@@ -32,7 +31,6 @@ Requires-Dist: mypy>=1.0.0; extra == "dev"
|
|
|
32
31
|
Provides-Extra: test
|
|
33
32
|
Requires-Dist: pytest>=7.0.0; extra == "test"
|
|
34
33
|
Requires-Dist: pytest-asyncio>=0.21.0; extra == "test"
|
|
35
|
-
Dynamic: license-file
|
|
36
34
|
|
|
37
35
|
# microsoft-agents-a365-tooling-extensions-azureaifoundry
|
|
38
36
|
|
|
@@ -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.
|
|
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
|
|
@@ -18,7 +18,6 @@ Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
|
18
18
|
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
19
19
|
Requires-Python: >=3.11
|
|
20
20
|
Description-Content-Type: text/markdown
|
|
21
|
-
License-File: LICENSE
|
|
22
21
|
Requires-Dist: microsoft-agents-a365-tooling>=0.0.0
|
|
23
22
|
Requires-Dist: azure-ai-projects>=2.0.0b1
|
|
24
23
|
Requires-Dist: azure-ai-agents>=1.0.0b251001
|
|
@@ -32,7 +31,6 @@ Requires-Dist: mypy>=1.0.0; extra == "dev"
|
|
|
32
31
|
Provides-Extra: test
|
|
33
32
|
Requires-Dist: pytest>=7.0.0; extra == "test"
|
|
34
33
|
Requires-Dist: pytest-asyncio>=0.21.0; extra == "test"
|
|
35
|
-
Dynamic: license-file
|
|
36
34
|
|
|
37
35
|
# microsoft-agents-a365-tooling-extensions-azureaifoundry
|
|
38
36
|
|
|
@@ -1,9 +1,6 @@
|
|
|
1
1
|
README.md
|
|
2
2
|
pyproject.toml
|
|
3
3
|
setup.py
|
|
4
|
-
../../LICENSE
|
|
5
|
-
docs/../../LICENSE
|
|
6
|
-
microsoft_agents_a365/../../LICENSE
|
|
7
4
|
microsoft_agents_a365/tooling/extensions/azureaifoundry/__init__.py
|
|
8
5
|
microsoft_agents_a365/tooling/extensions/azureaifoundry/services/__init__.py
|
|
9
6
|
microsoft_agents_a365/tooling/extensions/azureaifoundry/services/mcp_tool_registration_service.py
|
|
@@ -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)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|