Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,13 @@

from .environment_utils import get_observability_authentication_scope
from .power_platform_api_discovery import ClusterCategory, PowerPlatformApiDiscovery
from .utility import Utility

__all__ = [
"get_observability_authentication_scope",
"PowerPlatformApiDiscovery",
"ClusterCategory",
"Utility",
]

__path__ = __import__("pkgutil").extend_path(__path__, __name__)
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
# Copyright (c) Microsoft. All rights reserved.

"""
Utility functions for Microsoft Agent 365 runtime operations.

This module provides utility functions for token handling, agent identity resolution,
and other common runtime operations.
"""

from __future__ import annotations

import uuid
from typing import Any, Optional

import jwt


class Utility:
"""
Utility class providing common runtime operations for Agent 365.

This class contains static methods for token processing, agent identity resolution,
and other utility functions used across the Agent 365 runtime.
"""

@staticmethod
def get_app_id_from_token(token: Optional[str]) -> str:
"""
Decodes the current token and retrieves the App ID (appid or azp claim).

Args:
token: JWT token to decode. Can be None or empty.

Returns:
str: The App ID from the token's claims, or empty GUID if token is invalid.
Returns "00000000-0000-0000-0000-000000000000" if no valid App ID is found.
"""
if not token or not token.strip():
return str(uuid.UUID(int=0))

try:
# Decode the JWT token without verification (we only need the claims)
# Note: verify=False is used because we only need to extract claims,
# not verify the token's authenticity
decoded_payload = jwt.decode(token, options={"verify_signature": False})

# Look for appid or azp claims (appid takes precedence)
app_id = decoded_payload.get("appid") or decoded_payload.get("azp")
return app_id if app_id else ""

except (jwt.DecodeError, jwt.InvalidTokenError):
# Token is malformed or invalid
return ""

@staticmethod
def resolve_agent_identity(context: Any, auth_token: Optional[str]) -> str:
"""
Resolves the agent identity from the turn context or auth token.

Args:
context: Turn context of the conversation turn. Expected to have an Activity
with methods like is_agentic_request() and get_agentic_instance_id().
auth_token: Authentication token if available.

Returns:
str: The agent identity (App ID). Returns the agentic instance ID if the
request is agentic, otherwise returns the App ID from the auth token.
"""
try:
# App ID is required to pass to MCP server URL
# Try to get agentic instance ID if this is an agentic request
if context and context.activity and context.activity.is_agentic_request():
agentic_id = context.activity.get_agentic_instance_id()
return agentic_id if agentic_id else ""

except (AttributeError, TypeError, Exception):
# Context/activity doesn't have the expected methods or properties
# or any other error occurred while accessing context/activity
pass

# Fallback to extracting App ID from the auth token
return Utility.get_app_id_from_token(auth_token)
1 change: 1 addition & 0 deletions libraries/microsoft-agents-a365-runtime/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ classifiers = [
license = {text = "MIT"}
keywords = ["observability", "telemetry", "tracing", "opentelemetry", "monitoring", "ai", "agents"]
dependencies = [
"PyJWT >= 2.8.0",
]

[project.urls]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@

from microsoft_agents.hosting.core import Authorization, TurnContext

from microsoft_agents_a365.runtime.utility import Utility
from microsoft_agents_a365.tooling.services.mcp_tool_server_configuration_service import (
McpToolServerConfigurationService,
)
Expand Down Expand Up @@ -45,8 +46,8 @@ async def add_tool_servers_to_agent(
chat_client: Union[OpenAIChatClient, AzureOpenAIChatClient],
agent_instructions: str,
initial_tools: List[Any],
agentic_app_id: str,
auth: Authorization,
auth_handler_name: str,
turn_context: TurnContext,
auth_token: Optional[str] = None,
) -> Optional[ChatAgent]:
Expand All @@ -57,8 +58,8 @@ async def add_tool_servers_to_agent(
chat_client: The chat client instance (Union[OpenAIChatClient, AzureOpenAIChatClient])
agent_instructions: Instructions for the agent behavior
initial_tools: List of initial tools to add to the agent
agentic_app_id: Agentic app identifier for the agent
auth: Authorization context for token exchange
auth_handler_name: Name of the authorization handler.
turn_context: Turn context for the operation
auth_token: Optional bearer token for authentication

Expand All @@ -69,9 +70,11 @@ async def add_tool_servers_to_agent(
# Exchange token if not provided
if not auth_token:
scopes = get_mcp_platform_authentication_scope()
authToken = await auth.exchange_token(turn_context, scopes, "AGENTIC")
authToken = await auth.exchange_token(turn_context, scopes, auth_handler_name)
auth_token = authToken.token

agentic_app_id = Utility.resolve_agent_identity(turn_context, auth_token)

self._logger.info(f"Listing MCP tool servers for agent {agentic_app_id}")

# Get MCP server configurations
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
from azure.identity import DefaultAzureCredential
from azure.ai.agents.models import McpTool, ToolResources
from microsoft_agents.hosting.core import Authorization, TurnContext
from microsoft_agents_a365.runtime.utility import Utility
from microsoft_agents_a365.tooling.services.mcp_tool_server_configuration_service import (
McpToolServerConfigurationService,
)
Expand Down Expand Up @@ -69,8 +70,8 @@ def __init__(
async def add_tool_servers_to_agent(
self,
project_client: "AIProjectClient",
agentic_app_id: str,
auth: Authorization,
auth_handler_name: str,
context: TurnContext,
auth_token: Optional[str] = None,
) -> None:
Expand All @@ -79,7 +80,9 @@ async def add_tool_servers_to_agent(

Args:
project_client: The Azure Foundry AIProjectClient instance.
agentic_app_id: Agentic App ID for the agent.
auth: Authorization handler for token exchange.
auth_handler_name: Name of the authorization handler.
context: Turn context for the current operation.
auth_token: Authentication token to access the MCP servers.

Raises:
Expand All @@ -91,10 +94,11 @@ async def add_tool_servers_to_agent(

if not auth_token:
scopes = get_mcp_platform_authentication_scope()
authToken = await auth.exchange_token(context, scopes, "AGENTIC")
authToken = await auth.exchange_token(context, scopes, auth_handler_name)
auth_token = authToken.token

try:
agentic_app_id = Utility.resolve_agent_identity(context, auth_token)
# Get the tool definitions and resources using the async implementation
tool_definitions, tool_resources = await self._get_mcp_tool_definitions_and_resources(
agentic_app_id, auth_token or ""
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
MCPServerStreamableHttp,
MCPServerStreamableHttpParams,
)
from microsoft_agents_a365.runtime.utility import Utility
from microsoft_agents_a365.tooling.services.mcp_tool_server_configuration_service import (
McpToolServerConfigurationService,
)
Expand Down Expand Up @@ -50,8 +51,8 @@ def __init__(self, logger: Optional[logging.Logger] = None):
async def add_tool_servers_to_agent(
self,
agent: Agent,
agentic_app_id: str,
auth: Authorization,
auth_handler_name: str,
context: TurnContext,
auth_token: Optional[str] = None,
):
Expand All @@ -64,22 +65,25 @@ async def add_tool_servers_to_agent(

Args:
agent: The existing agent to add servers to
agentic_app_id: Agentic App ID for the agent
auth_token: Authentication token to access the MCP servers
auth: Authorization handler for token exchange.
auth_handler_name: Name of the authorization handler.
context: Turn context for the current operation.
auth_token: Authentication token to access the MCP servers.

Returns:
New Agent instance with all MCP servers, or original agent if no new servers
"""

if not auth_token:
scopes = get_mcp_platform_authentication_scope()
authToken = await auth.exchange_token(context, scopes, "AGENTIC")
authToken = await auth.exchange_token(context, scopes, auth_handler_name)
auth_token = authToken.token

# Get MCP server configurations from the configuration service
# mcp_server_configs = []
# TODO: radevika: Update once the common project is merged.

agentic_app_id = Utility.resolve_agent_identity(context, auth_token)
self._logger.info(f"Listing MCP tool servers for agent {agentic_app_id}")
mcp_server_configs = await self.config_service.list_tool_servers(
agentic_app_id=agentic_app_id,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
from semantic_kernel import kernel as sk
from semantic_kernel.connectors.mcp import MCPStreamableHttpPlugin
from microsoft_agents.hosting.core import Authorization, TurnContext
from microsoft_agents_a365.runtime.utility import Utility
from microsoft_agents_a365.tooling.services.mcp_tool_server_configuration_service import (
McpToolServerConfigurationService,
)
Expand Down Expand Up @@ -77,8 +78,8 @@ def __init__(
async def add_tool_servers_to_agent(
self,
kernel: sk.Kernel,
agentic_app_id: str,
auth: Authorization,
auth_handler_name: str,
context: TurnContext,
auth_token: Optional[str] = None,
) -> None:
Expand All @@ -87,7 +88,9 @@ async def add_tool_servers_to_agent(

Args:
kernel: The Semantic Kernel instance to which the tools will be added.
agentic_app_id: Agentic App ID for the agent.
auth: Authorization handler for token exchange.
auth_handler_name: Name of the authorization handler.
context: Turn context for the current operation.
auth_token: Authentication token to access the MCP servers.

Raises:
Expand All @@ -97,9 +100,10 @@ async def add_tool_servers_to_agent(

if not auth_token:
scopes = get_mcp_platform_authentication_scope()
authToken = await auth.exchange_token(context, scopes, "AGENTIC")
authToken = await auth.exchange_token(context, scopes, auth_handler_name)
auth_token = authToken.token

agentic_app_id = Utility.resolve_agent_identity(context, auth_token)
self._validate_inputs(kernel, agentic_app_id, auth_token)

# Get and process servers
Expand Down
Loading
Loading