microsoft-agents-a365-tooling 0.2.0.dev5__tar.gz → 0.2.1.dev2__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 (24) hide show
  1. {microsoft_agents_a365_tooling-0.2.0.dev5 → microsoft_agents_a365_tooling-0.2.1.dev2}/PKG-INFO +2 -1
  2. {microsoft_agents_a365_tooling-0.2.0.dev5 → microsoft_agents_a365_tooling-0.2.1.dev2}/microsoft_agents_a365/tooling/__init__.py +3 -0
  3. microsoft_agents_a365_tooling-0.2.1.dev2/microsoft_agents_a365/tooling/extensions/__init__.py +28 -0
  4. microsoft_agents_a365_tooling-0.2.1.dev2/microsoft_agents_a365/tooling/models/__init__.py +15 -0
  5. microsoft_agents_a365_tooling-0.2.1.dev2/microsoft_agents_a365/tooling/models/chat_history_message.py +49 -0
  6. microsoft_agents_a365_tooling-0.2.1.dev2/microsoft_agents_a365/tooling/models/chat_message_request.py +64 -0
  7. {microsoft_agents_a365_tooling-0.2.0.dev5 → microsoft_agents_a365_tooling-0.2.1.dev2}/microsoft_agents_a365/tooling/models/mcp_server_config.py +7 -1
  8. microsoft_agents_a365_tooling-0.2.1.dev2/microsoft_agents_a365/tooling/models/tool_options.py +17 -0
  9. microsoft_agents_a365_tooling-0.2.1.dev2/microsoft_agents_a365/tooling/py.typed +0 -0
  10. {microsoft_agents_a365_tooling-0.2.0.dev5 → microsoft_agents_a365_tooling-0.2.1.dev2}/microsoft_agents_a365/tooling/services/__init__.py +2 -1
  11. {microsoft_agents_a365_tooling-0.2.0.dev5 → microsoft_agents_a365_tooling-0.2.1.dev2}/microsoft_agents_a365/tooling/services/mcp_tool_server_configuration_service.py +241 -19
  12. {microsoft_agents_a365_tooling-0.2.0.dev5 → microsoft_agents_a365_tooling-0.2.1.dev2}/microsoft_agents_a365/tooling/utils/__init__.py +0 -2
  13. {microsoft_agents_a365_tooling-0.2.0.dev5 → microsoft_agents_a365_tooling-0.2.1.dev2}/microsoft_agents_a365/tooling/utils/constants.py +5 -1
  14. {microsoft_agents_a365_tooling-0.2.0.dev5 → microsoft_agents_a365_tooling-0.2.1.dev2}/microsoft_agents_a365/tooling/utils/utility.py +20 -37
  15. {microsoft_agents_a365_tooling-0.2.0.dev5 → microsoft_agents_a365_tooling-0.2.1.dev2}/microsoft_agents_a365_tooling.egg-info/PKG-INFO +2 -1
  16. {microsoft_agents_a365_tooling-0.2.0.dev5 → microsoft_agents_a365_tooling-0.2.1.dev2}/microsoft_agents_a365_tooling.egg-info/SOURCES.txt +5 -0
  17. {microsoft_agents_a365_tooling-0.2.0.dev5 → microsoft_agents_a365_tooling-0.2.1.dev2}/microsoft_agents_a365_tooling.egg-info/requires.txt +1 -0
  18. {microsoft_agents_a365_tooling-0.2.0.dev5 → microsoft_agents_a365_tooling-0.2.1.dev2}/microsoft_agents_a365_tooling.egg-info/top_level.txt +1 -0
  19. {microsoft_agents_a365_tooling-0.2.0.dev5 → microsoft_agents_a365_tooling-0.2.1.dev2}/pyproject.toml +6 -0
  20. {microsoft_agents_a365_tooling-0.2.0.dev5 → microsoft_agents_a365_tooling-0.2.1.dev2}/setup.py +1 -1
  21. microsoft_agents_a365_tooling-0.2.0.dev5/microsoft_agents_a365/tooling/models/__init__.py +0 -11
  22. {microsoft_agents_a365_tooling-0.2.0.dev5 → microsoft_agents_a365_tooling-0.2.1.dev2}/README.md +0 -0
  23. {microsoft_agents_a365_tooling-0.2.0.dev5 → microsoft_agents_a365_tooling-0.2.1.dev2}/microsoft_agents_a365_tooling.egg-info/dependency_links.txt +0 -0
  24. {microsoft_agents_a365_tooling-0.2.0.dev5 → microsoft_agents_a365_tooling-0.2.1.dev2}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: microsoft-agents-a365-tooling
3
- Version: 0.2.0.dev5
3
+ Version: 0.2.1.dev2
4
4
  Summary: Agent 365 Tooling SDK, providing functionality to work with Agent 365 Tools.
5
5
  Author-email: Microsoft <support@microsoft.com>
6
6
  License: MIT
@@ -20,6 +20,7 @@ Requires-Python: >=3.11
20
20
  Description-Content-Type: text/markdown
21
21
  Requires-Dist: pydantic>=2.0.0
22
22
  Requires-Dist: typing-extensions>=4.0.0
23
+ Requires-Dist: microsoft-agents-hosting-core<0.6.0,>=0.4.0
23
24
  Provides-Extra: dev
24
25
  Requires-Dist: pytest>=7.0.0; extra == "dev"
25
26
  Requires-Dist: pytest-asyncio>=0.21.0; extra == "dev"
@@ -27,3 +27,6 @@ __all__ = [
27
27
  "get_mcp_base_url",
28
28
  "build_mcp_server_url",
29
29
  ]
30
+
31
+ # Enable namespace package extension for tooling-extensions-* packages
32
+ __path__ = __import__("pkgutil").extend_path(__path__, __name__)
@@ -0,0 +1,28 @@
1
+ # Copyright (c) Microsoft Corporation.
2
+ # Licensed under the MIT License.
3
+
4
+ """Microsoft Agent 365 Tooling Extensions namespace package.
5
+
6
+ This file enables the `microsoft_agents_a365.tooling.extensions` namespace
7
+ to span multiple installed packages (e.g., extensions-openai, extensions-agentframework).
8
+ """
9
+
10
+ import sys
11
+ from pkgutil import extend_path
12
+
13
+ # Standard pkgutil-style namespace extension
14
+ __path__ = extend_path(__path__, __name__)
15
+
16
+ # For editable installs with custom finders, manually discover extension paths
17
+ for finder in sys.meta_path:
18
+ if hasattr(finder, "find_spec"):
19
+ try:
20
+ spec = finder.find_spec(__name__, None)
21
+ if spec is not None and spec.submodule_search_locations:
22
+ for path in spec.submodule_search_locations:
23
+ if path not in __path__ and not path.endswith(".__path_hook__"):
24
+ __path__.append(path)
25
+ except (ImportError, TypeError):
26
+ # Some meta path finders may not support this namespace and can raise
27
+ # ImportError or TypeError; ignore these and continue discovering paths.
28
+ pass
@@ -0,0 +1,15 @@
1
+ # Copyright (c) Microsoft Corporation.
2
+ # Licensed under the MIT License.
3
+
4
+ """
5
+ Common models for MCP tooling.
6
+
7
+ This module defines data models used across the MCP tooling framework.
8
+ """
9
+
10
+ from .chat_history_message import ChatHistoryMessage
11
+ from .chat_message_request import ChatMessageRequest
12
+ from .mcp_server_config import MCPServerConfig
13
+ from .tool_options import ToolOptions
14
+
15
+ __all__ = ["MCPServerConfig", "ToolOptions", "ChatHistoryMessage", "ChatMessageRequest"]
@@ -0,0 +1,49 @@
1
+ # Copyright (c) Microsoft Corporation.
2
+ # Licensed under the MIT License.
3
+
4
+ """Chat history message model."""
5
+
6
+ from datetime import datetime
7
+ from typing import Literal, Optional
8
+
9
+ from pydantic import BaseModel, ConfigDict, Field, field_validator
10
+
11
+
12
+ class ChatHistoryMessage(BaseModel):
13
+ """
14
+ Represents a single message in the chat history.
15
+
16
+ This model is used to capture individual messages exchanged between
17
+ users and the AI assistant for threat protection analysis and
18
+ compliance monitoring.
19
+
20
+ Attributes:
21
+ id: Optional unique identifier for the message.
22
+ role: The role of the message sender (user, assistant, or system).
23
+ content: The text content of the message.
24
+ timestamp: Optional timestamp when the message was created.
25
+
26
+ Example:
27
+ >>> message = ChatHistoryMessage(role="user", content="Hello, how can you help?")
28
+ >>> print(message.role)
29
+ 'user'
30
+ >>> print(message.content)
31
+ 'Hello, how can you help?'
32
+ """
33
+
34
+ model_config = ConfigDict(populate_by_name=True)
35
+
36
+ id: Optional[str] = Field(default=None, description="Unique message identifier")
37
+ role: Literal["user", "assistant", "system"] = Field(
38
+ ..., description="The role of the message sender"
39
+ )
40
+ content: str = Field(..., description="The message content")
41
+ timestamp: Optional[datetime] = Field(default=None, description="When the message was created")
42
+
43
+ @field_validator("content")
44
+ @classmethod
45
+ def content_not_empty(cls, v: str) -> str:
46
+ """Validate that content is not empty or whitespace-only."""
47
+ if not v or not v.strip():
48
+ raise ValueError("content cannot be empty or whitespace")
49
+ return v
@@ -0,0 +1,64 @@
1
+ # Copyright (c) Microsoft Corporation.
2
+ # Licensed under the MIT License.
3
+
4
+ """Chat message request model."""
5
+
6
+ from typing import List
7
+
8
+ from pydantic import BaseModel, ConfigDict, Field, field_validator
9
+
10
+ from .chat_history_message import ChatHistoryMessage
11
+
12
+
13
+ class ChatMessageRequest(BaseModel):
14
+ """
15
+ Request payload for sending chat history to MCP platform.
16
+
17
+ This model represents the complete request body sent to the MCP platform's
18
+ chat history endpoint for threat protection analysis. It includes the
19
+ current conversation context and historical messages.
20
+
21
+ The model uses field aliases to serialize to camelCase JSON format
22
+ as required by the MCP platform API.
23
+
24
+ Attributes:
25
+ conversation_id: Unique identifier for the conversation.
26
+ message_id: Unique identifier for the current message.
27
+ user_message: The current user message being processed.
28
+ chat_history: List of previous messages in the conversation.
29
+
30
+ Example:
31
+ >>> from microsoft_agents_a365.tooling.models import ChatHistoryMessage
32
+ >>> request = ChatMessageRequest(
33
+ ... conversation_id="conv-123",
34
+ ... message_id="msg-456",
35
+ ... user_message="What is the weather today?",
36
+ ... chat_history=[
37
+ ... ChatHistoryMessage(role="user", content="Hello"),
38
+ ... ChatHistoryMessage(role="assistant", content="Hi there!"),
39
+ ... ]
40
+ ... )
41
+ >>> # Serialize to camelCase JSON
42
+ >>> json_dict = request.model_dump(by_alias=True)
43
+ >>> print(json_dict["conversationId"])
44
+ 'conv-123'
45
+ """
46
+
47
+ model_config = ConfigDict(populate_by_name=True)
48
+
49
+ conversation_id: str = Field(
50
+ ..., alias="conversationId", description="Unique conversation identifier"
51
+ )
52
+ message_id: str = Field(..., alias="messageId", description="Current message identifier")
53
+ user_message: str = Field(..., alias="userMessage", description="The current user message")
54
+ chat_history: List[ChatHistoryMessage] = Field(
55
+ ..., alias="chatHistory", description="Previous messages in the conversation"
56
+ )
57
+
58
+ @field_validator("conversation_id", "message_id", "user_message")
59
+ @classmethod
60
+ def not_empty(cls, v: str) -> str:
61
+ """Validate that string fields are not empty or whitespace-only."""
62
+ if not v or not v.strip():
63
+ raise ValueError("Field cannot be empty or whitespace")
64
+ return v
@@ -1,10 +1,12 @@
1
- # Copyright (c) Microsoft. All rights reserved.
1
+ # Copyright (c) Microsoft Corporation.
2
+ # Licensed under the MIT License.
2
3
 
3
4
  """
4
5
  MCP Server Configuration model.
5
6
  """
6
7
 
7
8
  from dataclasses import dataclass
9
+ from typing import Optional
8
10
 
9
11
 
10
12
  @dataclass
@@ -19,6 +21,10 @@ class MCPServerConfig:
19
21
  #: Gets or sets the unique name of the MCP server.
20
22
  mcp_server_unique_name: str
21
23
 
24
+ #: Gets or sets the custom URL for the MCP server. If provided, this URL will be used
25
+ #: instead of constructing the URL from the base URL and unique name.
26
+ url: Optional[str] = None
27
+
22
28
  def __post_init__(self):
23
29
  """Validate the configuration after initialization."""
24
30
  if not self.mcp_server_name:
@@ -0,0 +1,17 @@
1
+ # Copyright (c) Microsoft Corporation.
2
+ # Licensed under the MIT License.
3
+
4
+ """
5
+ Tooling Options model.
6
+ """
7
+
8
+ from dataclasses import dataclass
9
+ from typing import Optional
10
+
11
+
12
+ @dataclass
13
+ class ToolOptions:
14
+ """Configuration options for tooling operations."""
15
+
16
+ #: Gets or sets the name of the orchestrator.
17
+ orchestrator_name: Optional[str]
@@ -1,4 +1,5 @@
1
- # Copyright (c) Microsoft. All rights reserved.
1
+ # Copyright (c) Microsoft Corporation.
2
+ # Licensed under the MIT License.
2
3
 
3
4
  """
4
5
  MCP tooling services package.
@@ -1,4 +1,5 @@
1
- # Copyright (c) Microsoft. All rights reserved.
1
+ # Copyright (c) Microsoft Corporation.
2
+ # Licensed under the MIT License.
2
3
 
3
4
  """
4
5
  MCP Tool Server Configuration Service.
@@ -17,20 +18,42 @@ The service supports both development and production scenarios:
17
18
  # ==============================================================================
18
19
 
19
20
  # Standard library imports
21
+ import asyncio
20
22
  import json
21
23
  import logging
22
24
  import os
23
25
  import sys
24
26
  from pathlib import Path
25
27
  from typing import Any, Dict, List, Optional
28
+ from urllib.parse import urlparse
26
29
 
27
30
  # Third-party imports
28
31
  import aiohttp
32
+ from microsoft_agents.hosting.core import TurnContext
29
33
 
30
34
  # Local imports
31
- from ..models import MCPServerConfig
35
+ from ..models import ChatHistoryMessage, ChatMessageRequest, MCPServerConfig, ToolOptions
32
36
  from ..utils import Constants
33
- from ..utils.utility import get_tooling_gateway_for_digital_worker, build_mcp_server_url
37
+ from ..utils.utility import (
38
+ get_tooling_gateway_for_digital_worker,
39
+ build_mcp_server_url,
40
+ get_chat_history_endpoint,
41
+ )
42
+
43
+ # Runtime Imports
44
+ from microsoft_agents_a365.runtime import OperationError, OperationResult
45
+ from microsoft_agents_a365.runtime.utility import Utility as RuntimeUtility
46
+
47
+
48
+ # ==============================================================================
49
+ # CONSTANTS
50
+ # ==============================================================================
51
+
52
+ # HTTP timeout in seconds for request operations
53
+ DEFAULT_REQUEST_TIMEOUT_SECONDS = 30
54
+
55
+ # HTTP status code for successful response
56
+ HTTP_STATUS_OK = 200
34
57
 
35
58
 
36
59
  # ==============================================================================
@@ -66,7 +89,7 @@ class McpToolServerConfigurationService:
66
89
  # --------------------------------------------------------------------------
67
90
 
68
91
  async def list_tool_servers(
69
- self, agentic_app_id: str, auth_token: str
92
+ self, agentic_app_id: str, auth_token: str, options: Optional[ToolOptions] = None
70
93
  ) -> List[MCPServerConfig]:
71
94
  """
72
95
  Gets the list of MCP Servers that are configured for the agent.
@@ -74,6 +97,7 @@ class McpToolServerConfigurationService:
74
97
  Args:
75
98
  agentic_app_id: Agentic App ID for the agent.
76
99
  auth_token: Authentication token to access the MCP servers.
100
+ options: Optional ToolOptions instance containing optional parameters.
77
101
 
78
102
  Returns:
79
103
  List[MCPServerConfig]: Returns the list of MCP Servers that are configured.
@@ -85,13 +109,17 @@ class McpToolServerConfigurationService:
85
109
  # Validate input parameters
86
110
  self._validate_input_parameters(agentic_app_id, auth_token)
87
111
 
112
+ # Use default options if none provided
113
+ if options is None:
114
+ options = ToolOptions(orchestrator_name=None)
115
+
88
116
  self._logger.info(f"Listing MCP tool servers for agent {agentic_app_id}")
89
117
 
90
118
  # Determine configuration source based on environment
91
119
  if self._is_development_scenario():
92
120
  return self._load_servers_from_manifest()
93
121
  else:
94
- return await self._load_servers_from_gateway(agentic_app_id, auth_token)
122
+ return await self._load_servers_from_gateway(agentic_app_id, auth_token, options)
95
123
 
96
124
  # --------------------------------------------------------------------------
97
125
  # ENVIRONMENT DETECTION
@@ -275,7 +303,7 @@ class McpToolServerConfigurationService:
275
303
  # --------------------------------------------------------------------------
276
304
 
277
305
  async def _load_servers_from_gateway(
278
- self, agentic_app_id: str, auth_token: str
306
+ self, agentic_app_id: str, auth_token: str, options: ToolOptions
279
307
  ) -> List[MCPServerConfig]:
280
308
  """
281
309
  Reads MCP server configurations from tooling gateway endpoint for production scenario.
@@ -283,6 +311,7 @@ class McpToolServerConfigurationService:
283
311
  Args:
284
312
  agentic_app_id: Agentic App ID for the agent.
285
313
  auth_token: Authentication token to access the tooling gateway.
314
+ options: ToolOptions instance containing optional parameters.
286
315
 
287
316
  Returns:
288
317
  List[MCPServerConfig]: List of MCP server configurations from tooling gateway.
@@ -294,7 +323,7 @@ class McpToolServerConfigurationService:
294
323
 
295
324
  try:
296
325
  config_endpoint = get_tooling_gateway_for_digital_worker(agentic_app_id)
297
- headers = self._prepare_gateway_headers(auth_token)
326
+ headers = self._prepare_gateway_headers(auth_token, options)
298
327
 
299
328
  self._logger.info(f"Calling tooling gateway endpoint: {config_endpoint}")
300
329
 
@@ -323,18 +352,22 @@ class McpToolServerConfigurationService:
323
352
 
324
353
  return mcp_servers
325
354
 
326
- def _prepare_gateway_headers(self, auth_token: str) -> Dict[str, str]:
355
+ def _prepare_gateway_headers(self, auth_token: str, options: ToolOptions) -> Dict[str, str]:
327
356
  """
328
357
  Prepares headers for tooling gateway requests.
329
358
 
330
359
  Args:
331
360
  auth_token: Authentication token.
361
+ options: ToolOptions instance containing optional parameters.
332
362
 
333
363
  Returns:
334
364
  Dictionary of HTTP headers.
335
365
  """
336
366
  return {
337
- "Authorization": f"{Constants.Headers.BEARER_PREFIX} {auth_token}",
367
+ Constants.Headers.AUTHORIZATION: f"{Constants.Headers.BEARER_PREFIX} {auth_token}",
368
+ Constants.Headers.USER_AGENT: RuntimeUtility.get_user_agent_header(
369
+ options.orchestrator_name
370
+ ),
338
371
  }
339
372
 
340
373
  async def _parse_gateway_response(
@@ -379,16 +412,26 @@ class McpToolServerConfigurationService:
379
412
  MCPServerConfig object or None if parsing fails.
380
413
  """
381
414
  try:
382
- name = self._extract_server_name(server_element)
383
- server_name = self._extract_server_unique_name(server_element)
415
+ mcp_server_name = self._extract_server_name(server_element)
416
+ mcp_server_unique_name = self._extract_server_unique_name(server_element)
384
417
 
385
- if not self._validate_server_strings(name, server_name):
418
+ if not self._validate_server_strings(mcp_server_name, mcp_server_unique_name):
386
419
  return None
387
420
 
388
- # Construct full URL using environment utilities
389
- full_url = build_mcp_server_url(server_name)
421
+ # Check if a URL is provided
422
+ endpoint = self._extract_server_url(server_element)
390
423
 
391
- return MCPServerConfig(mcp_server_name=name, mcp_server_unique_name=full_url)
424
+ # Use mcp_server_name if available, otherwise fall back to mcp_server_unique_name for URL construction
425
+ server_name = mcp_server_name or mcp_server_unique_name
426
+
427
+ # Determine the final URL: use custom URL if provided, otherwise construct it
428
+ final_url = endpoint if endpoint else build_mcp_server_url(server_name)
429
+
430
+ return MCPServerConfig(
431
+ mcp_server_name=mcp_server_name,
432
+ mcp_server_unique_name=mcp_server_unique_name,
433
+ url=final_url,
434
+ )
392
435
 
393
436
  except Exception:
394
437
  return None
@@ -406,13 +449,26 @@ class McpToolServerConfigurationService:
406
449
  MCPServerConfig object or None if parsing fails.
407
450
  """
408
451
  try:
409
- name = self._extract_server_name(server_element)
410
- endpoint = self._extract_server_unique_name(server_element)
452
+ mcp_server_name = self._extract_server_name(server_element)
453
+ mcp_server_unique_name = self._extract_server_unique_name(server_element)
411
454
 
412
- if not self._validate_server_strings(name, endpoint):
455
+ if not self._validate_server_strings(mcp_server_name, mcp_server_unique_name):
413
456
  return None
414
457
 
415
- return MCPServerConfig(mcp_server_name=name, mcp_server_unique_name=endpoint)
458
+ # Check if a URL is provided by the gateway
459
+ endpoint = self._extract_server_url(server_element)
460
+
461
+ # Use mcp_server_name if available, otherwise fall back to mcp_server_unique_name for URL construction
462
+ server_name = mcp_server_name or mcp_server_unique_name
463
+
464
+ # Determine the final URL: use custom URL if provided, otherwise construct it
465
+ final_url = endpoint if endpoint else build_mcp_server_url(server_name)
466
+
467
+ return MCPServerConfig(
468
+ mcp_server_name=mcp_server_name,
469
+ mcp_server_unique_name=mcp_server_unique_name,
470
+ url=final_url,
471
+ )
416
472
 
417
473
  except Exception:
418
474
  return None
@@ -467,6 +523,21 @@ class McpToolServerConfigurationService:
467
523
  return server_element["mcpServerUniqueName"]
468
524
  return None
469
525
 
526
+ def _extract_server_url(self, server_element: Dict[str, Any]) -> Optional[str]:
527
+ """
528
+ Extracts custom server URL from configuration element.
529
+
530
+ Args:
531
+ server_element: Configuration dictionary.
532
+
533
+ Returns:
534
+ Server URL string or None.
535
+ """
536
+ # Check for 'url' field in both manifest and gateway responses
537
+ if "url" in server_element and isinstance(server_element["url"], str):
538
+ return server_element["url"]
539
+ return None
540
+
470
541
  def _validate_server_strings(self, name: Optional[str], unique_name: Optional[str]) -> bool:
471
542
  """
472
543
  Validates that server name and unique name are valid strings.
@@ -479,3 +550,154 @@ class McpToolServerConfigurationService:
479
550
  True if both strings are valid, False otherwise.
480
551
  """
481
552
  return name is not None and name.strip() and unique_name is not None and unique_name.strip()
553
+
554
+ # --------------------------------------------------------------------------
555
+ # SEND CHAT HISTORY
556
+ # --------------------------------------------------------------------------
557
+
558
+ async def send_chat_history(
559
+ self,
560
+ turn_context: TurnContext,
561
+ chat_history_messages: List[ChatHistoryMessage],
562
+ options: Optional[ToolOptions] = None,
563
+ ) -> OperationResult:
564
+ """
565
+ Sends chat history to the MCP platform for real-time threat protection.
566
+
567
+ Args:
568
+ turn_context: TurnContext from the Agents SDK containing conversation information.
569
+ Must have a valid activity with conversation.id, activity.id, and
570
+ activity.text.
571
+ chat_history_messages: List of ChatHistoryMessage objects representing the chat
572
+ history. May be empty - an empty list will still send a
573
+ request to the MCP platform with empty chat history.
574
+ options: Optional ToolOptions instance containing optional parameters.
575
+
576
+ Returns:
577
+ OperationResult: An OperationResult indicating success or failure.
578
+ On success, returns OperationResult.success().
579
+ On failure, returns OperationResult.failed() with error details.
580
+
581
+ Raises:
582
+ ValueError: If turn_context is None, chat_history_messages is None,
583
+ turn_context.activity is None, or any of the required fields
584
+ (conversation.id, activity.id, activity.text) are missing or empty.
585
+
586
+ Note:
587
+ Even if chat_history_messages is empty, the request will still be sent to
588
+ the MCP platform. This ensures the user message from turn_context.activity.text
589
+ is registered correctly for real-time threat protection.
590
+
591
+ Example:
592
+ >>> from datetime import datetime, timezone
593
+ >>> from microsoft_agents_a365.tooling.models import ChatHistoryMessage
594
+ >>>
595
+ >>> history = [
596
+ ... ChatHistoryMessage("msg-1", "user", "Hello", datetime.now(timezone.utc)),
597
+ ... ChatHistoryMessage("msg-2", "assistant", "Hi!", datetime.now(timezone.utc))
598
+ ... ]
599
+ >>>
600
+ >>> service = McpToolServerConfigurationService()
601
+ >>> result = await service.send_chat_history(turn_context, history)
602
+ >>> if result.succeeded:
603
+ ... print("Chat history sent successfully")
604
+ """
605
+ # Validate input parameters
606
+ if turn_context is None:
607
+ raise ValueError("turn_context cannot be None")
608
+ if chat_history_messages is None:
609
+ raise ValueError("chat_history_messages cannot be None")
610
+
611
+ # Note: Empty chat_history_messages is allowed - we still send the request to MCP platform
612
+ # The platform needs to receive the request even with empty chat history
613
+
614
+ # Extract required information from turn context
615
+ if not turn_context.activity:
616
+ raise ValueError("turn_context.activity cannot be None")
617
+
618
+ conversation_id: Optional[str] = (
619
+ turn_context.activity.conversation.id if turn_context.activity.conversation else None
620
+ )
621
+ message_id: Optional[str] = turn_context.activity.id
622
+ user_message: Optional[str] = turn_context.activity.text
623
+
624
+ if conversation_id is None or (
625
+ isinstance(conversation_id, str) and not conversation_id.strip()
626
+ ):
627
+ raise ValueError(
628
+ "conversation_id cannot be empty or None (from turn_context.activity.conversation.id)"
629
+ )
630
+ if message_id is None or (isinstance(message_id, str) and not message_id.strip()):
631
+ raise ValueError("message_id cannot be empty or None (from turn_context.activity.id)")
632
+ if user_message is None or (isinstance(user_message, str) and not user_message.strip()):
633
+ raise ValueError(
634
+ "user_message cannot be empty or None (from turn_context.activity.text)"
635
+ )
636
+
637
+ # Use default options if none provided
638
+ if options is None:
639
+ options = ToolOptions(orchestrator_name=None)
640
+
641
+ # Get the endpoint URL
642
+ endpoint = get_chat_history_endpoint()
643
+
644
+ # Log only the URL path to avoid accidentally exposing sensitive data in query strings
645
+ parsed_url = urlparse(endpoint)
646
+ self._logger.debug(f"Sending chat history to endpoint path: {parsed_url.path}")
647
+
648
+ # Create the request payload
649
+ request = ChatMessageRequest(
650
+ conversation_id=conversation_id,
651
+ message_id=message_id,
652
+ user_message=user_message,
653
+ chat_history=chat_history_messages,
654
+ )
655
+
656
+ try:
657
+ # Prepare headers (no authentication required)
658
+ headers = {
659
+ Constants.Headers.USER_AGENT: RuntimeUtility.get_user_agent_header(
660
+ options.orchestrator_name
661
+ ),
662
+ "Content-Type": "application/json",
663
+ }
664
+
665
+ # Convert request to JSON (using Pydantic's model_dump with aliases for camelCase)
666
+ json_data = json.dumps(request.model_dump(by_alias=True, mode="json"))
667
+
668
+ # Send POST request with timeout to prevent indefinite hangs
669
+ timeout = aiohttp.ClientTimeout(total=DEFAULT_REQUEST_TIMEOUT_SECONDS)
670
+ async with aiohttp.ClientSession(timeout=timeout) as session:
671
+ async with session.post(endpoint, headers=headers, data=json_data) as response:
672
+ if response.status == HTTP_STATUS_OK:
673
+ self._logger.info("Successfully sent chat history to MCP platform")
674
+ return OperationResult.success()
675
+ else:
676
+ error_text = await response.text()
677
+ self._logger.error(
678
+ f"HTTP error sending chat history: HTTP {response.status}. "
679
+ f"Response: {error_text[:500]}"
680
+ )
681
+ # Use ClientResponseError for consistent error handling
682
+ http_error = aiohttp.ClientResponseError(
683
+ request_info=response.request_info,
684
+ history=response.history,
685
+ status=response.status,
686
+ message=error_text,
687
+ headers=response.headers,
688
+ )
689
+ return OperationResult.failed(OperationError(http_error))
690
+
691
+ except asyncio.TimeoutError as timeout_ex:
692
+ # Catch TimeoutError before ClientError since aiohttp.ServerTimeoutError
693
+ # inherits from both asyncio.TimeoutError and aiohttp.ClientError
694
+ self._logger.error(
695
+ f"Request timeout sending chat history to '{endpoint}': {str(timeout_ex)}"
696
+ )
697
+ return OperationResult.failed(OperationError(timeout_ex))
698
+ except aiohttp.ClientError as http_ex:
699
+ self._logger.error(f"HTTP error sending chat history to '{endpoint}': {str(http_ex)}")
700
+ return OperationResult.failed(OperationError(http_ex))
701
+ except Exception as ex:
702
+ self._logger.error(f"Failed to send chat history to '{endpoint}': {str(ex)}")
703
+ return OperationResult.failed(OperationError(ex))
@@ -10,7 +10,6 @@ from .utility import (
10
10
  get_tooling_gateway_for_digital_worker,
11
11
  get_mcp_base_url,
12
12
  build_mcp_server_url,
13
- get_tools_mode,
14
13
  get_mcp_platform_authentication_scope,
15
14
  )
16
15
 
@@ -19,6 +18,5 @@ __all__ = [
19
18
  "get_tooling_gateway_for_digital_worker",
20
19
  "get_mcp_base_url",
21
20
  "build_mcp_server_url",
22
- "get_tools_mode",
23
21
  "get_mcp_platform_authentication_scope",
24
22
  ]
@@ -1,4 +1,5 @@
1
- # Copyright (c) Microsoft. All rights reserved.
1
+ # Copyright (c) Microsoft Corporation.
2
+ # Licensed under the MIT License.
2
3
 
3
4
  """
4
5
  Provides constant values used throughout the Tooling components.
@@ -20,3 +21,6 @@ class Constants:
20
21
 
21
22
  #: The prefix used for Bearer authentication tokens in HTTP headers.
22
23
  BEARER_PREFIX = "Bearer"
24
+
25
+ #: The header name for User-Agent information.
26
+ USER_AGENT = "User-Agent"
@@ -1,23 +1,19 @@
1
- # Copyright (c) Microsoft. All rights reserved.
1
+ # Copyright (c) Microsoft Corporation.
2
+ # Licensed under the MIT License.
2
3
 
3
4
  """
4
5
  Provides utility functions for the Tooling components.
5
6
  """
6
7
 
7
8
  import os
8
- from enum import Enum
9
-
10
-
11
- class ToolsMode(Enum):
12
- """Enumeration for different tools modes."""
13
-
14
- MOCK_MCP_SERVER = "MockMCPServer"
15
- MCP_PLATFORM = "MCPPlatform"
16
9
 
17
10
 
18
11
  # Constants for base URLs
19
12
  MCP_PLATFORM_PROD_BASE_URL = "https://agent365.svc.cloud.microsoft"
20
13
 
14
+ # API endpoint paths
15
+ CHAT_HISTORY_ENDPOINT_PATH = "/agents/real-time-threat-protection/chat-message"
16
+
21
17
  PPAPI_TOKEN_SCOPE = "https://api.powerplatform.com"
22
18
  PROD_MCP_PLATFORM_AUTHENTICATION_SCOPE = "ea9ffc3e-8a23-4a7d-836d-234d7c7565c1/.default"
23
19
 
@@ -43,13 +39,6 @@ def get_mcp_base_url() -> str:
43
39
  Returns:
44
40
  str: The base URL for MCP servers.
45
41
  """
46
- environment = _get_current_environment().lower()
47
-
48
- if environment == "development":
49
- tools_mode = get_tools_mode()
50
- if tools_mode == ToolsMode.MOCK_MCP_SERVER:
51
- return os.getenv("MOCK_MCP_SERVER_URL", "http://localhost:5309/mcp-mock/agents/servers")
52
-
53
42
  return f"{_get_mcp_platform_base_url()}/agents/servers"
54
43
 
55
44
 
@@ -85,39 +74,33 @@ def _get_mcp_platform_base_url() -> str:
85
74
  Returns:
86
75
  str: The base URL for MCP platform.
87
76
  """
88
- if os.getenv("MCP_PLATFORM_ENDPOINT") is not None:
89
- return os.getenv("MCP_PLATFORM_ENDPOINT")
77
+ endpoint = os.getenv("MCP_PLATFORM_ENDPOINT")
78
+ if endpoint is not None:
79
+ return endpoint
90
80
 
91
81
  return MCP_PLATFORM_PROD_BASE_URL
92
82
 
93
83
 
94
- def get_tools_mode() -> ToolsMode:
84
+ def get_mcp_platform_authentication_scope() -> list[str]:
95
85
  """
96
- Gets the tools mode for the application.
86
+ Gets the MCP platform authentication scope.
97
87
 
98
88
  Returns:
99
- ToolsMode: The tools mode enum value.
89
+ list[str]: A list containing the appropriate MCP platform authentication scope.
100
90
  """
101
- tools_mode = os.getenv("TOOLS_MODE", "MCPPlatform").lower()
91
+ env_scope = os.getenv("MCP_PLATFORM_AUTHENTICATION_SCOPE", "")
102
92
 
103
- if tools_mode == "mockmcpserver":
104
- return ToolsMode.MOCK_MCP_SERVER
105
- else:
106
- return ToolsMode.MCP_PLATFORM
93
+ if env_scope:
94
+ return [env_scope]
95
+
96
+ return [PROD_MCP_PLATFORM_AUTHENTICATION_SCOPE]
107
97
 
108
98
 
109
- def get_mcp_platform_authentication_scope():
99
+ def get_chat_history_endpoint() -> str:
110
100
  """
111
- Gets the MCP platform authentication scope based on the current environment.
101
+ Gets the chat history endpoint URL for sending chat history to the MCP platform.
112
102
 
113
103
  Returns:
114
- list: A list containing the appropriate MCP platform authentication scope.
104
+ str: The chat history endpoint URL.
115
105
  """
116
- environment = _get_current_environment().lower()
117
-
118
- envScope = os.getenv("MCP_PLATFORM_AUTHENTICATION_SCOPE", "")
119
-
120
- if envScope:
121
- return [envScope]
122
-
123
- return [PROD_MCP_PLATFORM_AUTHENTICATION_SCOPE]
106
+ return f"{_get_mcp_platform_base_url()}{CHAT_HISTORY_ENDPOINT_PATH}"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: microsoft-agents-a365-tooling
3
- Version: 0.2.0.dev5
3
+ Version: 0.2.1.dev2
4
4
  Summary: Agent 365 Tooling SDK, providing functionality to work with Agent 365 Tools.
5
5
  Author-email: Microsoft <support@microsoft.com>
6
6
  License: MIT
@@ -20,6 +20,7 @@ Requires-Python: >=3.11
20
20
  Description-Content-Type: text/markdown
21
21
  Requires-Dist: pydantic>=2.0.0
22
22
  Requires-Dist: typing-extensions>=4.0.0
23
+ Requires-Dist: microsoft-agents-hosting-core<0.6.0,>=0.4.0
23
24
  Provides-Extra: dev
24
25
  Requires-Dist: pytest>=7.0.0; extra == "dev"
25
26
  Requires-Dist: pytest-asyncio>=0.21.0; extra == "dev"
@@ -2,8 +2,13 @@ README.md
2
2
  pyproject.toml
3
3
  setup.py
4
4
  microsoft_agents_a365/tooling/__init__.py
5
+ microsoft_agents_a365/tooling/py.typed
6
+ microsoft_agents_a365/tooling/extensions/__init__.py
5
7
  microsoft_agents_a365/tooling/models/__init__.py
8
+ microsoft_agents_a365/tooling/models/chat_history_message.py
9
+ microsoft_agents_a365/tooling/models/chat_message_request.py
6
10
  microsoft_agents_a365/tooling/models/mcp_server_config.py
11
+ microsoft_agents_a365/tooling/models/tool_options.py
7
12
  microsoft_agents_a365/tooling/services/__init__.py
8
13
  microsoft_agents_a365/tooling/services/mcp_tool_server_configuration_service.py
9
14
  microsoft_agents_a365/tooling/utils/__init__.py
@@ -1,5 +1,6 @@
1
1
  pydantic>=2.0.0
2
2
  typing-extensions>=4.0.0
3
+ microsoft-agents-hosting-core<0.6.0,>=0.4.0
3
4
 
4
5
  [dev]
5
6
  pytest>=7.0.0
@@ -25,6 +25,7 @@ license = {text = "MIT"}
25
25
  dependencies = [
26
26
  "pydantic >= 2.0.0",
27
27
  "typing-extensions >= 4.0.0",
28
+ "microsoft-agents-hosting-core >= 0.4.0, < 0.6.0",
28
29
  ]
29
30
 
30
31
  [project.urls]
@@ -51,6 +52,7 @@ include-package-data = true
51
52
 
52
53
  [tool.setuptools.package-data]
53
54
  "*" = ["../../LICENSE"]
55
+ "microsoft_agents_a365.tooling" = ["py.typed"]
54
56
 
55
57
  [tool.black]
56
58
  line-length = 100
@@ -60,6 +62,10 @@ target-version = ['py311']
60
62
  line-length = 100
61
63
  target-version = "py311"
62
64
 
65
+ [tool.ruff.lint.flake8-copyright]
66
+ notice-rgx = "# Copyright \\(c\\) Microsoft Corporation\\.\\r?\\n# Licensed under the MIT License\\."
67
+ min-file-size = 1
68
+
63
69
  [tool.mypy]
64
70
  python_version = "3.11"
65
71
  strict = true
@@ -13,7 +13,7 @@ package_version = environ.get("AGENT365_PYTHON_SDK_PACKAGE_VERSION", "0.0.0")
13
13
  helper_path = Path(__file__).parent.parent.parent / "versioning" / "helper"
14
14
  sys.path.insert(0, str(helper_path))
15
15
 
16
- from setup_utils import get_dynamic_dependencies
16
+ from setup_utils import get_dynamic_dependencies # noqa: E402
17
17
 
18
18
  # Use minimum version strategy:
19
19
  # - Internal packages get: >= current_base_version (e.g., >= 0.1.0)
@@ -1,11 +0,0 @@
1
- # Copyright (c) Microsoft. All rights reserved.
2
-
3
- """
4
- Common models for MCP tooling.
5
-
6
- This module defines data models used across the MCP tooling framework.
7
- """
8
-
9
- from .mcp_server_config import MCPServerConfig
10
-
11
- __all__ = ["MCPServerConfig"]