microsoft-agents-a365-tooling-extensions-semantickernel 0.2.1.dev7__tar.gz → 0.2.1.dev10__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_semantickernel-0.2.1.dev7 → microsoft_agents_a365_tooling_extensions_semantickernel-0.2.1.dev10}/PKG-INFO +1 -1
- microsoft_agents_a365_tooling_extensions_semantickernel-0.2.1.dev10/microsoft_agents_a365/tooling/extensions/semantickernel/services/mcp_tool_registration_service.py +565 -0
- {microsoft_agents_a365_tooling_extensions_semantickernel-0.2.1.dev7 → microsoft_agents_a365_tooling_extensions_semantickernel-0.2.1.dev10}/microsoft_agents_a365_tooling_extensions_semantickernel.egg-info/PKG-INFO +1 -1
- microsoft_agents_a365_tooling_extensions_semantickernel-0.2.1.dev7/microsoft_agents_a365/tooling/extensions/semantickernel/services/mcp_tool_registration_service.py +0 -205
- {microsoft_agents_a365_tooling_extensions_semantickernel-0.2.1.dev7 → microsoft_agents_a365_tooling_extensions_semantickernel-0.2.1.dev10}/README.md +0 -0
- {microsoft_agents_a365_tooling_extensions_semantickernel-0.2.1.dev7 → microsoft_agents_a365_tooling_extensions_semantickernel-0.2.1.dev10}/microsoft_agents_a365/tooling/extensions/semantickernel/__init__.py +0 -0
- {microsoft_agents_a365_tooling_extensions_semantickernel-0.2.1.dev7 → microsoft_agents_a365_tooling_extensions_semantickernel-0.2.1.dev10}/microsoft_agents_a365/tooling/extensions/semantickernel/services/__init__.py +0 -0
- {microsoft_agents_a365_tooling_extensions_semantickernel-0.2.1.dev7 → microsoft_agents_a365_tooling_extensions_semantickernel-0.2.1.dev10}/microsoft_agents_a365_tooling_extensions_semantickernel.egg-info/SOURCES.txt +0 -0
- {microsoft_agents_a365_tooling_extensions_semantickernel-0.2.1.dev7 → microsoft_agents_a365_tooling_extensions_semantickernel-0.2.1.dev10}/microsoft_agents_a365_tooling_extensions_semantickernel.egg-info/dependency_links.txt +0 -0
- {microsoft_agents_a365_tooling_extensions_semantickernel-0.2.1.dev7 → microsoft_agents_a365_tooling_extensions_semantickernel-0.2.1.dev10}/microsoft_agents_a365_tooling_extensions_semantickernel.egg-info/requires.txt +0 -0
- {microsoft_agents_a365_tooling_extensions_semantickernel-0.2.1.dev7 → microsoft_agents_a365_tooling_extensions_semantickernel-0.2.1.dev10}/microsoft_agents_a365_tooling_extensions_semantickernel.egg-info/top_level.txt +0 -0
- {microsoft_agents_a365_tooling_extensions_semantickernel-0.2.1.dev7 → microsoft_agents_a365_tooling_extensions_semantickernel-0.2.1.dev10}/pyproject.toml +0 -0
- {microsoft_agents_a365_tooling_extensions_semantickernel-0.2.1.dev7 → microsoft_agents_a365_tooling_extensions_semantickernel-0.2.1.dev10}/setup.cfg +0 -0
- {microsoft_agents_a365_tooling_extensions_semantickernel-0.2.1.dev7 → microsoft_agents_a365_tooling_extensions_semantickernel-0.2.1.dev10}/setup.py +0 -0
|
@@ -0,0 +1,565 @@
|
|
|
1
|
+
# Copyright (c) Microsoft Corporation.
|
|
2
|
+
# Licensed under the MIT License.
|
|
3
|
+
|
|
4
|
+
"""
|
|
5
|
+
MCP Tool Registration Service implementation for Semantic Kernel.
|
|
6
|
+
|
|
7
|
+
This module provides the concrete implementation of the MCP (Model Context Protocol)
|
|
8
|
+
tool registration service that integrates with Semantic Kernel to add MCP tool
|
|
9
|
+
servers to agents.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
# Standard library imports
|
|
13
|
+
import logging
|
|
14
|
+
import os
|
|
15
|
+
import re
|
|
16
|
+
import uuid
|
|
17
|
+
from datetime import datetime, timezone
|
|
18
|
+
from typing import Any, List, Optional, Sequence
|
|
19
|
+
|
|
20
|
+
# Third-party imports
|
|
21
|
+
from semantic_kernel import kernel as sk
|
|
22
|
+
from semantic_kernel.connectors.mcp import MCPStreamableHttpPlugin
|
|
23
|
+
from semantic_kernel.contents import AuthorRole, ChatHistory, ChatMessageContent
|
|
24
|
+
|
|
25
|
+
# Local imports
|
|
26
|
+
from microsoft_agents.hosting.core import Authorization, TurnContext
|
|
27
|
+
from microsoft_agents_a365.runtime import OperationError, OperationResult
|
|
28
|
+
from microsoft_agents_a365.runtime.utility import Utility
|
|
29
|
+
from microsoft_agents_a365.tooling.models import ChatHistoryMessage, ToolOptions
|
|
30
|
+
from microsoft_agents_a365.tooling.services.mcp_tool_server_configuration_service import (
|
|
31
|
+
McpToolServerConfigurationService,
|
|
32
|
+
)
|
|
33
|
+
from microsoft_agents_a365.tooling.utils.constants import Constants
|
|
34
|
+
from microsoft_agents_a365.tooling.utils.utility import (
|
|
35
|
+
get_mcp_platform_authentication_scope,
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class McpToolRegistrationService:
|
|
40
|
+
"""
|
|
41
|
+
Provides services related to tools in the Semantic Kernel.
|
|
42
|
+
|
|
43
|
+
This service handles registration and management of MCP (Model Context Protocol)
|
|
44
|
+
tool servers with Semantic Kernel agents.
|
|
45
|
+
"""
|
|
46
|
+
|
|
47
|
+
_orchestrator_name: str = "SemanticKernel"
|
|
48
|
+
|
|
49
|
+
def __init__(
|
|
50
|
+
self,
|
|
51
|
+
logger: Optional[logging.Logger] = None,
|
|
52
|
+
):
|
|
53
|
+
"""
|
|
54
|
+
Initialize the MCP Tool Registration Service for Semantic Kernel.
|
|
55
|
+
|
|
56
|
+
Args:
|
|
57
|
+
logger: Logger instance for logging operations.
|
|
58
|
+
"""
|
|
59
|
+
self._logger = logger or logging.getLogger(self.__class__.__name__)
|
|
60
|
+
self._mcp_server_configuration_service = McpToolServerConfigurationService(
|
|
61
|
+
logger=self._logger
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
# Store connected plugins to keep them alive
|
|
65
|
+
self._connected_plugins = []
|
|
66
|
+
|
|
67
|
+
# Enable debug logging if configured
|
|
68
|
+
if os.getenv("MCP_DEBUG_LOGGING", "false").lower() == "true":
|
|
69
|
+
self._logger.setLevel(logging.DEBUG)
|
|
70
|
+
|
|
71
|
+
# Configure strict parameter validation (prevents dynamic property creation)
|
|
72
|
+
self._strict_parameter_validation = (
|
|
73
|
+
os.getenv("MCP_STRICT_PARAMETER_VALIDATION", "true").lower() == "true"
|
|
74
|
+
)
|
|
75
|
+
if self._strict_parameter_validation:
|
|
76
|
+
self._logger.info(
|
|
77
|
+
"🔒 Strict parameter validation enabled - only schema-defined parameters are allowed"
|
|
78
|
+
)
|
|
79
|
+
else:
|
|
80
|
+
self._logger.info(
|
|
81
|
+
"🔓 Strict parameter validation disabled - dynamic parameters are allowed"
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
# ============================================================================
|
|
85
|
+
# Public Methods
|
|
86
|
+
# ============================================================================
|
|
87
|
+
|
|
88
|
+
async def add_tool_servers_to_agent(
|
|
89
|
+
self,
|
|
90
|
+
kernel: sk.Kernel,
|
|
91
|
+
auth: Authorization,
|
|
92
|
+
auth_handler_name: str,
|
|
93
|
+
context: TurnContext,
|
|
94
|
+
auth_token: Optional[str] = None,
|
|
95
|
+
) -> None:
|
|
96
|
+
"""
|
|
97
|
+
Adds the A365 MCP Tool Servers to the specified kernel.
|
|
98
|
+
|
|
99
|
+
Args:
|
|
100
|
+
kernel: The Semantic Kernel instance to which the tools will be added.
|
|
101
|
+
auth: Authorization handler for token exchange.
|
|
102
|
+
auth_handler_name: Name of the authorization handler.
|
|
103
|
+
context: Turn context for the current operation.
|
|
104
|
+
auth_token: Authentication token to access the MCP servers.
|
|
105
|
+
|
|
106
|
+
Raises:
|
|
107
|
+
ValueError: If kernel is None or required parameters are invalid.
|
|
108
|
+
Exception: If there's an error connecting to or configuring MCP servers.
|
|
109
|
+
"""
|
|
110
|
+
|
|
111
|
+
if not auth_token:
|
|
112
|
+
scopes = get_mcp_platform_authentication_scope()
|
|
113
|
+
authToken = await auth.exchange_token(context, scopes, auth_handler_name)
|
|
114
|
+
auth_token = authToken.token
|
|
115
|
+
|
|
116
|
+
agentic_app_id = Utility.resolve_agent_identity(context, auth_token)
|
|
117
|
+
self._validate_inputs(kernel, agentic_app_id, auth_token)
|
|
118
|
+
|
|
119
|
+
# Get and process servers
|
|
120
|
+
options = ToolOptions(orchestrator_name=self._orchestrator_name)
|
|
121
|
+
servers = await self._mcp_server_configuration_service.list_tool_servers(
|
|
122
|
+
agentic_app_id, auth_token, options
|
|
123
|
+
)
|
|
124
|
+
self._logger.info(f"🔧 Adding MCP tools from {len(servers)} servers")
|
|
125
|
+
|
|
126
|
+
# Process each server (matching C# foreach pattern)
|
|
127
|
+
for server in servers:
|
|
128
|
+
try:
|
|
129
|
+
headers = {
|
|
130
|
+
Constants.Headers.AUTHORIZATION: (
|
|
131
|
+
f"{Constants.Headers.BEARER_PREFIX} {auth_token}"
|
|
132
|
+
),
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
headers[Constants.Headers.USER_AGENT] = Utility.get_user_agent_header(
|
|
136
|
+
self._orchestrator_name
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
# Use the URL from server (always populated by the configuration service)
|
|
140
|
+
server_url = server.url
|
|
141
|
+
|
|
142
|
+
# Use mcp_server_name if available (not None or empty),
|
|
143
|
+
# otherwise fall back to mcp_server_unique_name
|
|
144
|
+
server_name = server.mcp_server_name or server.mcp_server_unique_name
|
|
145
|
+
|
|
146
|
+
plugin = MCPStreamableHttpPlugin(
|
|
147
|
+
name=server_name,
|
|
148
|
+
url=server_url,
|
|
149
|
+
headers=headers,
|
|
150
|
+
)
|
|
151
|
+
|
|
152
|
+
# Connect the plugin
|
|
153
|
+
await plugin.connect()
|
|
154
|
+
|
|
155
|
+
# Add plugin to kernel
|
|
156
|
+
kernel.add_plugin(plugin, server_name)
|
|
157
|
+
|
|
158
|
+
# Store reference to keep plugin alive throughout application lifecycle.
|
|
159
|
+
# By storing plugin references in _connected_plugins, we prevent
|
|
160
|
+
# Python's garbage collector from cleaning up the plugin objects.
|
|
161
|
+
# The connections remain active throughout the application lifecycle.
|
|
162
|
+
# Tools can be invoked because their underlying connections stay alive.
|
|
163
|
+
self._connected_plugins.append(plugin)
|
|
164
|
+
|
|
165
|
+
self._logger.info(
|
|
166
|
+
f"✅ Connected and added MCP plugin for: {server.mcp_server_name}"
|
|
167
|
+
)
|
|
168
|
+
|
|
169
|
+
except Exception as e:
|
|
170
|
+
self._logger.error(f"Failed to add tools from {server.mcp_server_name}: {str(e)}")
|
|
171
|
+
|
|
172
|
+
self._logger.info("✅ Successfully configured MCP tool servers for the agent!")
|
|
173
|
+
|
|
174
|
+
# ============================================================================
|
|
175
|
+
# Private Methods - Input Validation & Processing
|
|
176
|
+
# ============================================================================
|
|
177
|
+
|
|
178
|
+
def _validate_inputs(self, kernel: Any, agentic_app_id: str, auth_token: str) -> None:
|
|
179
|
+
"""Validate all required inputs."""
|
|
180
|
+
if kernel is None:
|
|
181
|
+
raise ValueError("kernel cannot be None")
|
|
182
|
+
if not agentic_app_id or not agentic_app_id.strip():
|
|
183
|
+
raise ValueError("agentic_app_id cannot be null or empty")
|
|
184
|
+
if not auth_token or not auth_token.strip():
|
|
185
|
+
raise ValueError("auth_token cannot be null or empty")
|
|
186
|
+
|
|
187
|
+
# ============================================================================
|
|
188
|
+
# Private Methods - Kernel Function Creation
|
|
189
|
+
# ============================================================================
|
|
190
|
+
|
|
191
|
+
def _get_plugin_name_from_server_name(self, server_name: str) -> str:
|
|
192
|
+
"""Generate a clean plugin name from server name."""
|
|
193
|
+
clean_name = re.sub(r"[^a-zA-Z0-9_]", "_", server_name)
|
|
194
|
+
return f"{clean_name}Tools"
|
|
195
|
+
|
|
196
|
+
# ============================================================================
|
|
197
|
+
# SEND CHAT HISTORY - Semantic Kernel-specific implementations
|
|
198
|
+
# ============================================================================
|
|
199
|
+
|
|
200
|
+
async def send_chat_history(
|
|
201
|
+
self,
|
|
202
|
+
turn_context: TurnContext,
|
|
203
|
+
chat_history: ChatHistory,
|
|
204
|
+
limit: Optional[int] = None,
|
|
205
|
+
options: Optional[ToolOptions] = None,
|
|
206
|
+
) -> OperationResult:
|
|
207
|
+
"""
|
|
208
|
+
Send Semantic Kernel chat history to the MCP platform.
|
|
209
|
+
|
|
210
|
+
This method extracts messages from a Semantic Kernel ChatHistory object,
|
|
211
|
+
converts them to ChatHistoryMessage format, and sends them to the MCP
|
|
212
|
+
platform for real-time threat protection.
|
|
213
|
+
|
|
214
|
+
Args:
|
|
215
|
+
turn_context: TurnContext from the Agents SDK containing conversation info.
|
|
216
|
+
chat_history: Semantic Kernel ChatHistory object containing messages.
|
|
217
|
+
limit: Optional maximum number of messages to send. If specified,
|
|
218
|
+
sends the most recent N messages. If None, sends all messages.
|
|
219
|
+
options: Optional configuration for the request.
|
|
220
|
+
|
|
221
|
+
Returns:
|
|
222
|
+
OperationResult indicating success or failure.
|
|
223
|
+
|
|
224
|
+
Raises:
|
|
225
|
+
ValueError: If turn_context or chat_history is None.
|
|
226
|
+
|
|
227
|
+
Example:
|
|
228
|
+
>>> from semantic_kernel.contents import ChatHistory
|
|
229
|
+
>>> from microsoft_agents_a365.tooling.extensions.semantickernel import (
|
|
230
|
+
... McpToolRegistrationService
|
|
231
|
+
... )
|
|
232
|
+
>>>
|
|
233
|
+
>>> service = McpToolRegistrationService()
|
|
234
|
+
>>> chat_history = ChatHistory()
|
|
235
|
+
>>> chat_history.add_user_message("Hello!")
|
|
236
|
+
>>> chat_history.add_assistant_message("Hi there!")
|
|
237
|
+
>>>
|
|
238
|
+
>>> result = await service.send_chat_history(
|
|
239
|
+
... turn_context, chat_history, limit=50
|
|
240
|
+
... )
|
|
241
|
+
>>> if result.succeeded:
|
|
242
|
+
... print("Chat history sent successfully")
|
|
243
|
+
"""
|
|
244
|
+
# Validate inputs
|
|
245
|
+
if turn_context is None:
|
|
246
|
+
raise ValueError("turn_context cannot be None")
|
|
247
|
+
if chat_history is None:
|
|
248
|
+
raise ValueError("chat_history cannot be None")
|
|
249
|
+
|
|
250
|
+
try:
|
|
251
|
+
# Extract messages from ChatHistory
|
|
252
|
+
messages = list(chat_history.messages)
|
|
253
|
+
self._logger.debug(f"Extracted {len(messages)} messages from ChatHistory")
|
|
254
|
+
|
|
255
|
+
# Apply limit if specified
|
|
256
|
+
if limit is not None and limit > 0 and len(messages) > limit:
|
|
257
|
+
self._logger.info(f"Applying limit of {limit} to {len(messages)} messages")
|
|
258
|
+
messages = messages[-limit:] # Take the most recent N messages
|
|
259
|
+
|
|
260
|
+
# Delegate to the list-based method
|
|
261
|
+
return await self.send_chat_history_messages(
|
|
262
|
+
turn_context=turn_context,
|
|
263
|
+
messages=messages,
|
|
264
|
+
options=options,
|
|
265
|
+
)
|
|
266
|
+
except ValueError:
|
|
267
|
+
# Re-raise validation errors
|
|
268
|
+
raise
|
|
269
|
+
except Exception as ex:
|
|
270
|
+
self._logger.error(f"Failed to send chat history: {ex}")
|
|
271
|
+
return OperationResult.failed(OperationError(ex))
|
|
272
|
+
|
|
273
|
+
async def send_chat_history_messages(
|
|
274
|
+
self,
|
|
275
|
+
turn_context: TurnContext,
|
|
276
|
+
messages: Sequence[ChatMessageContent],
|
|
277
|
+
options: Optional[ToolOptions] = None,
|
|
278
|
+
) -> OperationResult:
|
|
279
|
+
"""
|
|
280
|
+
Send Semantic Kernel chat history messages to the MCP platform.
|
|
281
|
+
|
|
282
|
+
This method accepts a sequence of Semantic Kernel ChatMessageContent objects,
|
|
283
|
+
converts them to ChatHistoryMessage format, and sends them to the MCP
|
|
284
|
+
platform for real-time threat protection.
|
|
285
|
+
|
|
286
|
+
Args:
|
|
287
|
+
turn_context: TurnContext from the Agents SDK containing conversation info.
|
|
288
|
+
messages: Sequence of Semantic Kernel ChatMessageContent objects to send.
|
|
289
|
+
options: Optional configuration for the request.
|
|
290
|
+
|
|
291
|
+
Returns:
|
|
292
|
+
OperationResult indicating success or failure.
|
|
293
|
+
|
|
294
|
+
Raises:
|
|
295
|
+
ValueError: If turn_context or messages is None.
|
|
296
|
+
|
|
297
|
+
Example:
|
|
298
|
+
>>> from semantic_kernel.contents import ChatMessageContent, AuthorRole
|
|
299
|
+
>>> from microsoft_agents_a365.tooling.extensions.semantickernel import (
|
|
300
|
+
... McpToolRegistrationService
|
|
301
|
+
... )
|
|
302
|
+
>>>
|
|
303
|
+
>>> service = McpToolRegistrationService()
|
|
304
|
+
>>> messages = [
|
|
305
|
+
... ChatMessageContent(role=AuthorRole.USER, content="Hello!"),
|
|
306
|
+
... ChatMessageContent(role=AuthorRole.ASSISTANT, content="Hi there!"),
|
|
307
|
+
... ]
|
|
308
|
+
>>>
|
|
309
|
+
>>> result = await service.send_chat_history_messages(
|
|
310
|
+
... turn_context, messages
|
|
311
|
+
... )
|
|
312
|
+
>>> if result.succeeded:
|
|
313
|
+
... print("Chat history sent successfully")
|
|
314
|
+
"""
|
|
315
|
+
# Validate inputs
|
|
316
|
+
if turn_context is None:
|
|
317
|
+
raise ValueError("turn_context cannot be None")
|
|
318
|
+
if messages is None:
|
|
319
|
+
raise ValueError("messages cannot be None")
|
|
320
|
+
|
|
321
|
+
self._logger.info(f"Sending {len(messages)} Semantic Kernel messages as chat history")
|
|
322
|
+
|
|
323
|
+
# Set default options
|
|
324
|
+
if options is None:
|
|
325
|
+
options = ToolOptions(orchestrator_name=self._orchestrator_name)
|
|
326
|
+
elif options.orchestrator_name is None:
|
|
327
|
+
options.orchestrator_name = self._orchestrator_name
|
|
328
|
+
|
|
329
|
+
try:
|
|
330
|
+
# Convert Semantic Kernel messages to ChatHistoryMessage format
|
|
331
|
+
chat_history_messages = self._convert_sk_messages_to_chat_history(messages)
|
|
332
|
+
|
|
333
|
+
# Call core service even with empty chat_history_messages
|
|
334
|
+
if len(chat_history_messages) == 0:
|
|
335
|
+
self._logger.info(
|
|
336
|
+
"Empty chat history messages (either no input or all filtered), "
|
|
337
|
+
"still sending to register user message"
|
|
338
|
+
)
|
|
339
|
+
|
|
340
|
+
self._logger.debug(
|
|
341
|
+
f"Converted {len(chat_history_messages)} messages to ChatHistoryMessage format"
|
|
342
|
+
)
|
|
343
|
+
|
|
344
|
+
# Delegate to core service
|
|
345
|
+
return await self._mcp_server_configuration_service.send_chat_history(
|
|
346
|
+
turn_context=turn_context,
|
|
347
|
+
chat_history_messages=chat_history_messages,
|
|
348
|
+
options=options,
|
|
349
|
+
)
|
|
350
|
+
except ValueError:
|
|
351
|
+
# Re-raise validation errors from the core service
|
|
352
|
+
raise
|
|
353
|
+
except Exception as ex:
|
|
354
|
+
self._logger.error(f"Failed to send chat history messages: {ex}")
|
|
355
|
+
return OperationResult.failed(OperationError(ex))
|
|
356
|
+
|
|
357
|
+
# ============================================================================
|
|
358
|
+
# PRIVATE HELPER METHODS - Message Conversion
|
|
359
|
+
# ============================================================================
|
|
360
|
+
|
|
361
|
+
def _convert_sk_messages_to_chat_history(
|
|
362
|
+
self,
|
|
363
|
+
messages: Sequence[ChatMessageContent],
|
|
364
|
+
) -> List[ChatHistoryMessage]:
|
|
365
|
+
"""
|
|
366
|
+
Convert Semantic Kernel ChatMessageContent objects to ChatHistoryMessage format.
|
|
367
|
+
|
|
368
|
+
Args:
|
|
369
|
+
messages: Sequence of Semantic Kernel ChatMessageContent objects.
|
|
370
|
+
|
|
371
|
+
Returns:
|
|
372
|
+
List of ChatHistoryMessage objects. Messages that cannot be converted
|
|
373
|
+
are filtered out with a warning log.
|
|
374
|
+
"""
|
|
375
|
+
chat_history_messages: List[ChatHistoryMessage] = []
|
|
376
|
+
|
|
377
|
+
for idx, message in enumerate(messages):
|
|
378
|
+
converted = self._convert_single_sk_message(message, idx)
|
|
379
|
+
if converted is not None:
|
|
380
|
+
chat_history_messages.append(converted)
|
|
381
|
+
|
|
382
|
+
self._logger.info(
|
|
383
|
+
f"Converted {len(chat_history_messages)} of {len(messages)} messages "
|
|
384
|
+
"to ChatHistoryMessage format"
|
|
385
|
+
)
|
|
386
|
+
return chat_history_messages
|
|
387
|
+
|
|
388
|
+
def _convert_single_sk_message(
|
|
389
|
+
self,
|
|
390
|
+
message: ChatMessageContent,
|
|
391
|
+
index: int = 0,
|
|
392
|
+
) -> Optional[ChatHistoryMessage]:
|
|
393
|
+
"""
|
|
394
|
+
Convert a single Semantic Kernel message to ChatHistoryMessage format.
|
|
395
|
+
|
|
396
|
+
Args:
|
|
397
|
+
message: Single Semantic Kernel ChatMessageContent message.
|
|
398
|
+
index: Index of the message in the list (for logging).
|
|
399
|
+
|
|
400
|
+
Returns:
|
|
401
|
+
ChatHistoryMessage object or None if conversion fails.
|
|
402
|
+
"""
|
|
403
|
+
try:
|
|
404
|
+
# Skip None messages
|
|
405
|
+
if message is None:
|
|
406
|
+
self._logger.warning(f"Skipping null message at index {index}")
|
|
407
|
+
return None
|
|
408
|
+
|
|
409
|
+
# Map role to string
|
|
410
|
+
role = self._map_author_role(message.role)
|
|
411
|
+
|
|
412
|
+
# Extract content
|
|
413
|
+
content = self._extract_content(message)
|
|
414
|
+
if not content or not content.strip():
|
|
415
|
+
self._logger.warning(f"Skipping message at index {index} with empty content")
|
|
416
|
+
return None
|
|
417
|
+
|
|
418
|
+
# Extract or generate ID
|
|
419
|
+
msg_id = self._extract_or_generate_id(message, index)
|
|
420
|
+
|
|
421
|
+
# Extract or generate timestamp
|
|
422
|
+
timestamp = self._extract_or_generate_timestamp(message, index)
|
|
423
|
+
|
|
424
|
+
self._logger.debug(
|
|
425
|
+
f"Converting message {index}: role={role}, "
|
|
426
|
+
f"id={msg_id}, has_timestamp={timestamp is not None}"
|
|
427
|
+
)
|
|
428
|
+
|
|
429
|
+
return ChatHistoryMessage(
|
|
430
|
+
id=msg_id,
|
|
431
|
+
role=role,
|
|
432
|
+
content=content,
|
|
433
|
+
timestamp=timestamp,
|
|
434
|
+
)
|
|
435
|
+
except Exception as ex:
|
|
436
|
+
self._logger.error(f"Failed to convert message at index {index}: {ex}")
|
|
437
|
+
return None
|
|
438
|
+
|
|
439
|
+
def _map_author_role(self, role: AuthorRole) -> str:
|
|
440
|
+
"""
|
|
441
|
+
Map Semantic Kernel AuthorRole enum to lowercase string.
|
|
442
|
+
|
|
443
|
+
Args:
|
|
444
|
+
role: AuthorRole enum value.
|
|
445
|
+
|
|
446
|
+
Returns:
|
|
447
|
+
Lowercase string representation of the role.
|
|
448
|
+
"""
|
|
449
|
+
return role.name.lower()
|
|
450
|
+
|
|
451
|
+
def _extract_content(self, message: ChatMessageContent) -> str:
|
|
452
|
+
"""
|
|
453
|
+
Extract text content from a ChatMessageContent.
|
|
454
|
+
|
|
455
|
+
Args:
|
|
456
|
+
message: Semantic Kernel ChatMessageContent object.
|
|
457
|
+
|
|
458
|
+
Returns:
|
|
459
|
+
Content string (may be empty). Returns empty string for unexpected
|
|
460
|
+
types to avoid unintentionally exposing sensitive data.
|
|
461
|
+
"""
|
|
462
|
+
content = message.content
|
|
463
|
+
|
|
464
|
+
if content is None:
|
|
465
|
+
return ""
|
|
466
|
+
|
|
467
|
+
# If content is already a string, return it directly
|
|
468
|
+
if isinstance(content, str):
|
|
469
|
+
return content
|
|
470
|
+
|
|
471
|
+
# For unexpected types, log a warning and return empty string to avoid
|
|
472
|
+
# unintentionally stringifying objects that might contain sensitive data
|
|
473
|
+
content_type = type(content).__name__
|
|
474
|
+
self._logger.warning(
|
|
475
|
+
f"Unexpected content type '{content_type}' encountered. "
|
|
476
|
+
"Returning empty string to avoid potential data exposure."
|
|
477
|
+
)
|
|
478
|
+
return ""
|
|
479
|
+
|
|
480
|
+
def _extract_or_generate_id(
|
|
481
|
+
self,
|
|
482
|
+
message: ChatMessageContent,
|
|
483
|
+
index: int,
|
|
484
|
+
) -> str:
|
|
485
|
+
"""
|
|
486
|
+
Extract message ID from metadata or generate a new UUID.
|
|
487
|
+
|
|
488
|
+
Args:
|
|
489
|
+
message: Semantic Kernel ChatMessageContent object.
|
|
490
|
+
index: Message index for logging.
|
|
491
|
+
|
|
492
|
+
Returns:
|
|
493
|
+
Message ID string.
|
|
494
|
+
"""
|
|
495
|
+
# Try to get existing ID from metadata
|
|
496
|
+
if message.metadata and "id" in message.metadata:
|
|
497
|
+
existing_id = message.metadata["id"]
|
|
498
|
+
if existing_id:
|
|
499
|
+
return str(existing_id)
|
|
500
|
+
|
|
501
|
+
# Generate new UUID
|
|
502
|
+
generated_id = str(uuid.uuid4())
|
|
503
|
+
self._logger.debug(f"Generated UUID {generated_id} for message at index {index}")
|
|
504
|
+
return generated_id
|
|
505
|
+
|
|
506
|
+
def _extract_or_generate_timestamp(
|
|
507
|
+
self,
|
|
508
|
+
message: ChatMessageContent,
|
|
509
|
+
index: int,
|
|
510
|
+
) -> datetime:
|
|
511
|
+
"""
|
|
512
|
+
Extract timestamp from metadata or generate current UTC time.
|
|
513
|
+
|
|
514
|
+
Args:
|
|
515
|
+
message: Semantic Kernel ChatMessageContent object.
|
|
516
|
+
index: Message index for logging.
|
|
517
|
+
|
|
518
|
+
Returns:
|
|
519
|
+
Timestamp as datetime object.
|
|
520
|
+
"""
|
|
521
|
+
# Try to get existing timestamp from metadata
|
|
522
|
+
if message.metadata:
|
|
523
|
+
existing_timestamp = message.metadata.get("timestamp") or message.metadata.get(
|
|
524
|
+
"created_at"
|
|
525
|
+
)
|
|
526
|
+
if existing_timestamp:
|
|
527
|
+
if isinstance(existing_timestamp, datetime):
|
|
528
|
+
return existing_timestamp
|
|
529
|
+
elif isinstance(existing_timestamp, (int, float)):
|
|
530
|
+
# Unix timestamp
|
|
531
|
+
return datetime.fromtimestamp(existing_timestamp, tz=timezone.utc)
|
|
532
|
+
elif isinstance(existing_timestamp, str):
|
|
533
|
+
try:
|
|
534
|
+
return datetime.fromisoformat(existing_timestamp.replace("Z", "+00:00"))
|
|
535
|
+
except (ValueError, TypeError) as ex:
|
|
536
|
+
self._logger.debug(
|
|
537
|
+
f"Failed to parse timestamp '{existing_timestamp}' at index {index}: {ex}"
|
|
538
|
+
)
|
|
539
|
+
|
|
540
|
+
# Use current UTC time
|
|
541
|
+
self._logger.debug(f"Using current UTC time for message at index {index}")
|
|
542
|
+
return datetime.now(timezone.utc)
|
|
543
|
+
|
|
544
|
+
# ============================================================================
|
|
545
|
+
# Cleanup Methods
|
|
546
|
+
# ============================================================================
|
|
547
|
+
|
|
548
|
+
async def cleanup_connections(self) -> None:
|
|
549
|
+
"""Clean up all connected MCP plugins."""
|
|
550
|
+
self._logger.info(f"🧹 Cleaning up {len(self._connected_plugins)} MCP plugin connections")
|
|
551
|
+
|
|
552
|
+
for plugin in self._connected_plugins:
|
|
553
|
+
try:
|
|
554
|
+
if hasattr(plugin, "close"):
|
|
555
|
+
await plugin.close()
|
|
556
|
+
elif hasattr(plugin, "disconnect"):
|
|
557
|
+
await plugin.disconnect()
|
|
558
|
+
self._logger.debug(
|
|
559
|
+
f"✅ Closed connection for plugin: {getattr(plugin, 'name', 'unknown')}"
|
|
560
|
+
)
|
|
561
|
+
except Exception as e:
|
|
562
|
+
self._logger.warning(f"⚠️ Error closing plugin connection: {e}")
|
|
563
|
+
|
|
564
|
+
self._connected_plugins.clear()
|
|
565
|
+
self._logger.info("✅ All MCP plugin connections cleaned up")
|
|
@@ -1,205 +0,0 @@
|
|
|
1
|
-
# Copyright (c) Microsoft Corporation.
|
|
2
|
-
# Licensed under the MIT License.
|
|
3
|
-
|
|
4
|
-
"""
|
|
5
|
-
MCP Tool Registration Service implementation for Semantic Kernel.
|
|
6
|
-
|
|
7
|
-
This module provides the concrete implementation of the MCP (Model Context Protocol)
|
|
8
|
-
tool registration service that integrates with Semantic Kernel to add MCP tool
|
|
9
|
-
servers to agents.
|
|
10
|
-
"""
|
|
11
|
-
|
|
12
|
-
# Standard library imports
|
|
13
|
-
import logging
|
|
14
|
-
import os
|
|
15
|
-
import re
|
|
16
|
-
from typing import Any, Optional
|
|
17
|
-
from semantic_kernel import kernel as sk
|
|
18
|
-
from semantic_kernel.connectors.mcp import MCPStreamableHttpPlugin
|
|
19
|
-
from microsoft_agents.hosting.core import Authorization, TurnContext
|
|
20
|
-
from microsoft_agents_a365.runtime.utility import Utility
|
|
21
|
-
from microsoft_agents_a365.tooling.services.mcp_tool_server_configuration_service import (
|
|
22
|
-
McpToolServerConfigurationService,
|
|
23
|
-
)
|
|
24
|
-
from microsoft_agents_a365.tooling.models import ToolOptions
|
|
25
|
-
from microsoft_agents_a365.tooling.utils.constants import Constants
|
|
26
|
-
from microsoft_agents_a365.tooling.utils.utility import (
|
|
27
|
-
get_mcp_platform_authentication_scope,
|
|
28
|
-
)
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
class McpToolRegistrationService:
|
|
32
|
-
"""
|
|
33
|
-
Provides services related to tools in the Semantic Kernel.
|
|
34
|
-
|
|
35
|
-
This service handles registration and management of MCP (Model Context Protocol)
|
|
36
|
-
tool servers with Semantic Kernel agents.
|
|
37
|
-
"""
|
|
38
|
-
|
|
39
|
-
_orchestrator_name: str = "SemanticKernel"
|
|
40
|
-
|
|
41
|
-
def __init__(
|
|
42
|
-
self,
|
|
43
|
-
logger: Optional[logging.Logger] = None,
|
|
44
|
-
):
|
|
45
|
-
"""
|
|
46
|
-
Initialize the MCP Tool Registration Service for Semantic Kernel.
|
|
47
|
-
|
|
48
|
-
Args:
|
|
49
|
-
logger: Logger instance for logging operations.
|
|
50
|
-
"""
|
|
51
|
-
self._logger = logger or logging.getLogger(self.__class__.__name__)
|
|
52
|
-
self._mcp_server_configuration_service = McpToolServerConfigurationService(
|
|
53
|
-
logger=self._logger
|
|
54
|
-
)
|
|
55
|
-
|
|
56
|
-
# Store connected plugins to keep them alive
|
|
57
|
-
self._connected_plugins = []
|
|
58
|
-
|
|
59
|
-
# Enable debug logging if configured
|
|
60
|
-
if os.getenv("MCP_DEBUG_LOGGING", "false").lower() == "true":
|
|
61
|
-
self._logger.setLevel(logging.DEBUG)
|
|
62
|
-
|
|
63
|
-
# Configure strict parameter validation (prevents dynamic property creation)
|
|
64
|
-
self._strict_parameter_validation = (
|
|
65
|
-
os.getenv("MCP_STRICT_PARAMETER_VALIDATION", "true").lower() == "true"
|
|
66
|
-
)
|
|
67
|
-
if self._strict_parameter_validation:
|
|
68
|
-
self._logger.info(
|
|
69
|
-
"🔒 Strict parameter validation enabled - only schema-defined parameters are allowed"
|
|
70
|
-
)
|
|
71
|
-
else:
|
|
72
|
-
self._logger.info(
|
|
73
|
-
"🔓 Strict parameter validation disabled - dynamic parameters are allowed"
|
|
74
|
-
)
|
|
75
|
-
|
|
76
|
-
# ============================================================================
|
|
77
|
-
# Public Methods
|
|
78
|
-
# ============================================================================
|
|
79
|
-
|
|
80
|
-
async def add_tool_servers_to_agent(
|
|
81
|
-
self,
|
|
82
|
-
kernel: sk.Kernel,
|
|
83
|
-
auth: Authorization,
|
|
84
|
-
auth_handler_name: str,
|
|
85
|
-
context: TurnContext,
|
|
86
|
-
auth_token: Optional[str] = None,
|
|
87
|
-
) -> None:
|
|
88
|
-
"""
|
|
89
|
-
Adds the A365 MCP Tool Servers to the specified kernel.
|
|
90
|
-
|
|
91
|
-
Args:
|
|
92
|
-
kernel: The Semantic Kernel instance to which the tools will be added.
|
|
93
|
-
auth: Authorization handler for token exchange.
|
|
94
|
-
auth_handler_name: Name of the authorization handler.
|
|
95
|
-
context: Turn context for the current operation.
|
|
96
|
-
auth_token: Authentication token to access the MCP servers.
|
|
97
|
-
|
|
98
|
-
Raises:
|
|
99
|
-
ValueError: If kernel is None or required parameters are invalid.
|
|
100
|
-
Exception: If there's an error connecting to or configuring MCP servers.
|
|
101
|
-
"""
|
|
102
|
-
|
|
103
|
-
if not auth_token:
|
|
104
|
-
scopes = get_mcp_platform_authentication_scope()
|
|
105
|
-
authToken = await auth.exchange_token(context, scopes, auth_handler_name)
|
|
106
|
-
auth_token = authToken.token
|
|
107
|
-
|
|
108
|
-
agentic_app_id = Utility.resolve_agent_identity(context, auth_token)
|
|
109
|
-
self._validate_inputs(kernel, agentic_app_id, auth_token)
|
|
110
|
-
|
|
111
|
-
# Get and process servers
|
|
112
|
-
options = ToolOptions(orchestrator_name=self._orchestrator_name)
|
|
113
|
-
servers = await self._mcp_server_configuration_service.list_tool_servers(
|
|
114
|
-
agentic_app_id, auth_token, options
|
|
115
|
-
)
|
|
116
|
-
self._logger.info(f"🔧 Adding MCP tools from {len(servers)} servers")
|
|
117
|
-
|
|
118
|
-
# Process each server (matching C# foreach pattern)
|
|
119
|
-
for server in servers:
|
|
120
|
-
try:
|
|
121
|
-
headers = {
|
|
122
|
-
Constants.Headers.AUTHORIZATION: f"{Constants.Headers.BEARER_PREFIX} {auth_token}",
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
headers[Constants.Headers.USER_AGENT] = Utility.get_user_agent_header(
|
|
126
|
-
self._orchestrator_name
|
|
127
|
-
)
|
|
128
|
-
|
|
129
|
-
# Use the URL from server (always populated by the configuration service)
|
|
130
|
-
server_url = server.url
|
|
131
|
-
|
|
132
|
-
# Use mcp_server_name if available (not None or empty), otherwise fall back to mcp_server_unique_name
|
|
133
|
-
server_name = server.mcp_server_name or server.mcp_server_unique_name
|
|
134
|
-
|
|
135
|
-
plugin = MCPStreamableHttpPlugin(
|
|
136
|
-
name=server_name,
|
|
137
|
-
url=server_url,
|
|
138
|
-
headers=headers,
|
|
139
|
-
)
|
|
140
|
-
|
|
141
|
-
# Connect the plugin
|
|
142
|
-
await plugin.connect()
|
|
143
|
-
|
|
144
|
-
# Add plugin to kernel
|
|
145
|
-
kernel.add_plugin(plugin, server_name)
|
|
146
|
-
|
|
147
|
-
# Store reference to keep plugin alive throughout application lifecycle
|
|
148
|
-
# By storing plugin references in _connected_plugins, we prevent Python's garbage collector from cleaning up the plugin objects
|
|
149
|
-
# The connections remain active throughout the application lifecycle
|
|
150
|
-
# Tools can be successfully invoked because their underlying connections are still alive
|
|
151
|
-
self._connected_plugins.append(plugin)
|
|
152
|
-
|
|
153
|
-
self._logger.info(
|
|
154
|
-
f"✅ Connected and added MCP plugin for: {server.mcp_server_name}"
|
|
155
|
-
)
|
|
156
|
-
|
|
157
|
-
except Exception as e:
|
|
158
|
-
self._logger.error(f"Failed to add tools from {server.mcp_server_name}: {str(e)}")
|
|
159
|
-
|
|
160
|
-
self._logger.info("✅ Successfully configured MCP tool servers for the agent!")
|
|
161
|
-
|
|
162
|
-
# ============================================================================
|
|
163
|
-
# Private Methods - Input Validation & Processing
|
|
164
|
-
# ============================================================================
|
|
165
|
-
|
|
166
|
-
def _validate_inputs(self, kernel: Any, agentic_app_id: str, auth_token: str) -> None:
|
|
167
|
-
"""Validate all required inputs."""
|
|
168
|
-
if kernel is None:
|
|
169
|
-
raise ValueError("kernel cannot be None")
|
|
170
|
-
if not agentic_app_id or not agentic_app_id.strip():
|
|
171
|
-
raise ValueError("agentic_app_id cannot be null or empty")
|
|
172
|
-
if not auth_token or not auth_token.strip():
|
|
173
|
-
raise ValueError("auth_token cannot be null or empty")
|
|
174
|
-
|
|
175
|
-
# ============================================================================
|
|
176
|
-
# Private Methods - Kernel Function Creation
|
|
177
|
-
# ============================================================================
|
|
178
|
-
|
|
179
|
-
def _get_plugin_name_from_server_name(self, server_name: str) -> str:
|
|
180
|
-
"""Generate a clean plugin name from server name."""
|
|
181
|
-
clean_name = re.sub(r"[^a-zA-Z0-9_]", "_", server_name)
|
|
182
|
-
return f"{clean_name}Tools"
|
|
183
|
-
|
|
184
|
-
# ============================================================================
|
|
185
|
-
# Cleanup Methods
|
|
186
|
-
# ============================================================================
|
|
187
|
-
|
|
188
|
-
async def cleanup_connections(self) -> None:
|
|
189
|
-
"""Clean up all connected MCP plugins."""
|
|
190
|
-
self._logger.info(f"🧹 Cleaning up {len(self._connected_plugins)} MCP plugin connections")
|
|
191
|
-
|
|
192
|
-
for plugin in self._connected_plugins:
|
|
193
|
-
try:
|
|
194
|
-
if hasattr(plugin, "close"):
|
|
195
|
-
await plugin.close()
|
|
196
|
-
elif hasattr(plugin, "disconnect"):
|
|
197
|
-
await plugin.disconnect()
|
|
198
|
-
self._logger.debug(
|
|
199
|
-
f"✅ Closed connection for plugin: {getattr(plugin, 'name', 'unknown')}"
|
|
200
|
-
)
|
|
201
|
-
except Exception as e:
|
|
202
|
-
self._logger.warning(f"⚠️ Error closing plugin connection: {e}")
|
|
203
|
-
|
|
204
|
-
self._connected_plugins.clear()
|
|
205
|
-
self._logger.info("✅ All MCP plugin connections cleaned up")
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|