microsoft-agents-a365-tooling-extensions-semantickernel 0.2.1.dev7__tar.gz → 0.2.1.dev9__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_semantickernel-0.2.1.dev7 → microsoft_agents_a365_tooling_extensions_semantickernel-0.2.1.dev9}/PKG-INFO +1 -1
  2. microsoft_agents_a365_tooling_extensions_semantickernel-0.2.1.dev9/microsoft_agents_a365/tooling/extensions/semantickernel/services/mcp_tool_registration_service.py +565 -0
  3. {microsoft_agents_a365_tooling_extensions_semantickernel-0.2.1.dev7 → microsoft_agents_a365_tooling_extensions_semantickernel-0.2.1.dev9}/microsoft_agents_a365_tooling_extensions_semantickernel.egg-info/PKG-INFO +1 -1
  4. microsoft_agents_a365_tooling_extensions_semantickernel-0.2.1.dev7/microsoft_agents_a365/tooling/extensions/semantickernel/services/mcp_tool_registration_service.py +0 -205
  5. {microsoft_agents_a365_tooling_extensions_semantickernel-0.2.1.dev7 → microsoft_agents_a365_tooling_extensions_semantickernel-0.2.1.dev9}/README.md +0 -0
  6. {microsoft_agents_a365_tooling_extensions_semantickernel-0.2.1.dev7 → microsoft_agents_a365_tooling_extensions_semantickernel-0.2.1.dev9}/microsoft_agents_a365/tooling/extensions/semantickernel/__init__.py +0 -0
  7. {microsoft_agents_a365_tooling_extensions_semantickernel-0.2.1.dev7 → microsoft_agents_a365_tooling_extensions_semantickernel-0.2.1.dev9}/microsoft_agents_a365/tooling/extensions/semantickernel/services/__init__.py +0 -0
  8. {microsoft_agents_a365_tooling_extensions_semantickernel-0.2.1.dev7 → microsoft_agents_a365_tooling_extensions_semantickernel-0.2.1.dev9}/microsoft_agents_a365_tooling_extensions_semantickernel.egg-info/SOURCES.txt +0 -0
  9. {microsoft_agents_a365_tooling_extensions_semantickernel-0.2.1.dev7 → microsoft_agents_a365_tooling_extensions_semantickernel-0.2.1.dev9}/microsoft_agents_a365_tooling_extensions_semantickernel.egg-info/dependency_links.txt +0 -0
  10. {microsoft_agents_a365_tooling_extensions_semantickernel-0.2.1.dev7 → microsoft_agents_a365_tooling_extensions_semantickernel-0.2.1.dev9}/microsoft_agents_a365_tooling_extensions_semantickernel.egg-info/requires.txt +0 -0
  11. {microsoft_agents_a365_tooling_extensions_semantickernel-0.2.1.dev7 → microsoft_agents_a365_tooling_extensions_semantickernel-0.2.1.dev9}/microsoft_agents_a365_tooling_extensions_semantickernel.egg-info/top_level.txt +0 -0
  12. {microsoft_agents_a365_tooling_extensions_semantickernel-0.2.1.dev7 → microsoft_agents_a365_tooling_extensions_semantickernel-0.2.1.dev9}/pyproject.toml +0 -0
  13. {microsoft_agents_a365_tooling_extensions_semantickernel-0.2.1.dev7 → microsoft_agents_a365_tooling_extensions_semantickernel-0.2.1.dev9}/setup.cfg +0 -0
  14. {microsoft_agents_a365_tooling_extensions_semantickernel-0.2.1.dev7 → microsoft_agents_a365_tooling_extensions_semantickernel-0.2.1.dev9}/setup.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: microsoft-agents-a365-tooling-extensions-semantickernel
3
- Version: 0.2.1.dev7
3
+ Version: 0.2.1.dev9
4
4
  Summary: Semantic Kernel integration for Agent 365 Tooling SDK
5
5
  Author-email: Microsoft <support@microsoft.com>
6
6
  License: MIT
@@ -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,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: microsoft-agents-a365-tooling-extensions-semantickernel
3
- Version: 0.2.1.dev7
3
+ Version: 0.2.1.dev9
4
4
  Summary: Semantic Kernel integration for Agent 365 Tooling SDK
5
5
  Author-email: Microsoft <support@microsoft.com>
6
6
  License: MIT
@@ -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")