Skip to content
Merged
Show file tree
Hide file tree
Changes from 22 commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
e7e68fe
Python: Add Scaffolding for Durable AzureFunctions package to Agent F…
larohra Nov 3, 2025
0808fd2
Merge branch 'main' into feature-azure-functions
dmytrostruk Nov 5, 2025
1d5677b
Merge branch 'main' into feature-azure-functions
dmytrostruk Nov 5, 2025
5686a00
.NET: Durable extension: initial src and unit tests (#1900)
cgillum Nov 5, 2025
1762cda
Python: Add Durable Agent Wrapper code (#1913)
larohra Nov 6, 2025
90742ba
Azure Functions .NET samples (#1939)
cgillum Nov 6, 2025
754491c
Python: Add Unit tests for Azurefunctions package (#1976)
larohra Nov 7, 2025
40b6def
.NET: [Feature Branch] Migrate state schema updates and support for a…
cgillum Nov 7, 2025
0aa8d30
Python: Add more samples for Azure Functions (#1980)
larohra Nov 7, 2025
9150176
Agents as MCP tools
gavin-aguiar Nov 7, 2025
a6b6659
Removed unused files and updated sample
gavin-aguiar Nov 10, 2025
304b809
.NET: [Feature Branch] Durable Task extension integration tests (#2017)
cgillum Nov 10, 2025
916b51f
.NET: [Feature Branch] Update OpenAI config for integration tests (#2…
cgillum Nov 10, 2025
4eb31f1
Python: Add Integration tests for AzureFunctions (#2020)
larohra Nov 11, 2025
24a45c8
Merge branch 'feature-azure-functions' of https://github.com/microsof…
gavin-aguiar Nov 11, 2025
40104e4
Addressed copilot feedback
gavin-aguiar Nov 11, 2025
d4f66e3
durable mcp tool
gavin-aguiar Nov 21, 2025
1fbbbf0
Minor refactoring and added tests
gavin-aguiar Nov 21, 2025
dcb7f87
Merge branch 'main' of https://github.com/microsoft/agent-framework i…
gavin-aguiar Nov 21, 2025
284cd5a
Updated mcp sample
gavin-aguiar Nov 21, 2025
8d5b8d1
Fixed broken link in readme
gavin-aguiar Nov 21, 2025
6fe2c40
Addressed copilot comments
gavin-aguiar Nov 21, 2025
a5655c5
Merge branch 'main' into gaaguiar/mcp
gavin-aguiar Nov 21, 2025
b5e513a
Addressed feedback
gavin-aguiar Nov 24, 2025
68040eb
Merge branch 'gaaguiar/mcp' of https://github.com/microsoft/agent-fra…
gavin-aguiar Nov 24, 2025
617e86f
Merge branch 'main' into gaaguiar/mcp
gavin-aguiar Nov 24, 2025
4bc0e87
Updated property to enable_mcp_tool_trigger
gavin-aguiar Nov 24, 2025
f626f1b
Merge branch 'gaaguiar/mcp' of https://github.com/microsoft/agent-fra…
gavin-aguiar Nov 24, 2025
2cb8636
Merge branch 'main' into gaaguiar/mcp
gavin-aguiar Nov 24, 2025
f35b668
Merge branch 'main' into gaaguiar/mcp
gavin-aguiar Nov 25, 2025
9d79ed5
Merge branch 'main' into gaaguiar/mcp
gavin-aguiar Nov 25, 2025
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 @@ -56,6 +56,15 @@ def orchestration_trigger(self, context_name: str) -> Callable[[HandlerT], Handl

def activity_trigger(self, input_name: str) -> Callable[[HandlerT], HandlerT]: ...

def mcp_tool_trigger(
self,
arg_name: str,
tool_name: str,
description: str,
tool_properties: str,
data_type: func.DataType,
) -> Callable[[HandlerT], HandlerT]: ...

else:
DFAppBase = df.DFApp # type: ignore[assignment]

Expand Down Expand Up @@ -117,14 +126,17 @@ def my_orchestration(context):
agents: Dictionary of agent name to AgentProtocol instance
enable_health_check: Whether health check endpoint is enabled
enable_http_endpoints: Whether HTTP endpoints are created for agents
enable_mcp_tool_endpoint: Whether MCP tool endpoints are created for agents
max_poll_retries: Maximum polling attempts when waiting for responses
poll_interval_seconds: Delay (seconds) between polling attempts
"""

agents: dict[str, AgentProtocol]
enable_health_check: bool
enable_http_endpoints: bool
enable_mcp_tool_endpoint: bool
agent_http_endpoint_flags: dict[str, bool]
agent_mcp_tool_flags: dict[str, bool]

def __init__(
self,
Expand All @@ -134,6 +146,7 @@ def __init__(
enable_http_endpoints: bool = True,
max_poll_retries: int = DEFAULT_MAX_POLL_RETRIES,
poll_interval_seconds: float = DEFAULT_POLL_INTERVAL_SECONDS,
enable_mcp_tool_endpoint: bool = False,
default_callback: AgentResponseCallbackProtocol | None = None,
):
"""Initialize the AgentFunctionApp.
Expand All @@ -142,6 +155,8 @@ def __init__(
:param http_auth_level: HTTP authentication level (default: ``func.AuthLevel.FUNCTION``).
:param enable_health_check: Enable the built-in health check endpoint (default: ``True``).
:param enable_http_endpoints: Enable HTTP endpoints for agents (default: ``True``).
:param enable_mcp_tool_endpoint: Enable MCP tool endpoints for agents (default: ``False``).
When enabled, agents will be exposed as MCP tools that can be invoked by MCP-compatible clients.
:param max_poll_retries: Maximum polling attempts when waiting for a response.
Defaults to ``DEFAULT_MAX_POLL_RETRIES``.
:param poll_interval_seconds: Delay in seconds between polling attempts.
Expand All @@ -158,8 +173,10 @@ def __init__(
# Initialize agents dictionary
self.agents = {}
self.agent_http_endpoint_flags = {}
self.agent_mcp_tool_flags = {}
self.enable_health_check = enable_health_check
self.enable_http_endpoints = enable_http_endpoints
self.enable_mcp_tool_endpoint = enable_mcp_tool_endpoint
self.default_callback = default_callback

try:
Expand Down Expand Up @@ -191,15 +208,18 @@ def add_agent(
agent: AgentProtocol,
callback: AgentResponseCallbackProtocol | None = None,
enable_http_endpoint: bool | None = None,
enable_mcp_tool_endpoint: bool | None = None,
) -> None:
"""Add an agent to the function app after initialization.

Args:
agent: The Microsoft Agent Framework agent instance (must implement AgentProtocol)
The agent must have a 'name' attribute.
callback: Optional callback invoked during agent execution
enable_http_endpoint: Optional flag that overrides the app-level
HTTP endpoint setting for this agent
enable_http_endpoint: Optional flag to enable/disable HTTP endpoint for this agent.
If None, uses the app-level enable_http_endpoints setting.
enable_mcp_tool_endpoint: Optional flag to enable/disable MCP tool endpoint for this agent.
If None, uses the app-level enable_mcp_tool_endpoint setting.

Raises:
ValueError: If the agent doesn't have a 'name' attribute or if an agent
Expand All @@ -216,6 +236,11 @@ def add_agent(
effective_enable_http_endpoint = (
self.enable_http_endpoints if enable_http_endpoint is None else self._coerce_to_bool(enable_http_endpoint)
)
effective_enable_mcp_endpoint = (
self.enable_mcp_tool_endpoint
if enable_mcp_tool_endpoint is None
else self._coerce_to_bool(enable_mcp_tool_endpoint)
)

logger.debug(f"[AgentFunctionApp] Adding agent: {name}")
logger.debug(f"[AgentFunctionApp] Route: /api/agents/{name}")
Expand All @@ -224,17 +249,18 @@ def add_agent(
"enabled" if effective_enable_http_endpoint else "disabled",
name,
)
logger.debug(
f"[AgentFunctionApp] MCP tool trigger: {'enabled' if effective_enable_mcp_endpoint else 'disabled'}"
)

self.agents[name] = agent
self.agent_mcp_tool_flags[name] = effective_enable_mcp_endpoint
self.agent_http_endpoint_flags[name] = effective_enable_http_endpoint

effective_callback = callback or self.default_callback

self._setup_agent_functions(
agent,
name,
effective_callback,
effective_enable_http_endpoint,
agent, name, effective_callback, effective_enable_http_endpoint, effective_enable_mcp_endpoint
)

logger.debug(f"[AgentFunctionApp] Agent '{name}' added successfully")
Expand Down Expand Up @@ -269,15 +295,16 @@ def _setup_agent_functions(
agent_name: str,
callback: AgentResponseCallbackProtocol | None,
enable_http_endpoint: bool,
enable_mcp_tool_endpoint: bool,
) -> None:
"""Set up the HTTP trigger and entity for a specific agent.
"""Set up the HTTP trigger, entity, and MCP tool trigger for a specific agent.

Args:
agent: The agent instance
agent_name: The name to use for routing and entity registration
callback: Optional callback to receive response updates
enable_http_endpoint: Whether the HTTP run route is enabled for
this agent
enable_http_endpoint: Whether to create HTTP endpoint
enable_mcp_tool_endpoint: Whether to create MCP tool endpoint
"""
logger.debug(f"[AgentFunctionApp] Setting up functions for agent '{agent_name}'...")

Expand All @@ -290,6 +317,12 @@ def _setup_agent_functions(
)
self._setup_agent_entity(agent, agent_name, callback)

if enable_mcp_tool_endpoint:
agent_description = getattr(agent, "description", None)
self._setup_mcp_tool_trigger(agent_name, agent_description)
else:
logger.debug(f"[AgentFunctionApp] MCP tool trigger disabled for agent '{agent_name}'")

def _setup_http_run_route(self, agent_name: str) -> None:
"""Register the POST route that triggers agent execution.

Expand Down Expand Up @@ -448,6 +481,162 @@ def entity_function(context: df.DurableEntityContext) -> None:
entity_function.__name__ = entity_name_with_prefix
self.entity_trigger(context_name="context", entity_name=entity_name_with_prefix)(entity_function)

def _setup_mcp_tool_trigger(self, agent_name: str, agent_description: str | None) -> None:
"""Register an MCP tool trigger for an agent using Azure Functions native MCP support.

This creates a native Azure Functions MCP tool trigger that exposes the agent
as an MCP tool, allowing it to be invoked by MCP-compatible clients.

Args:
agent_name: The agent name (used as the MCP tool name)
agent_description: Optional description for the MCP tool (shown to clients)
"""
mcp_function_name = self._build_function_name(agent_name, "mcptool")

# Define tool properties as JSON (MCP tool parameters)
tool_properties = json.dumps([
{
"propertyName": "query",
"propertyType": "string",
"description": "The query to send to the agent.",
"isRequired": True,
"isArray": False,
},
{
"propertyName": "threadId",
"propertyType": "string",
"description": "Optional thread identifier for conversation continuity.",
"isRequired": False,
"isArray": False,
},
])

function_name_decorator = self.function_name(mcp_function_name)
mcp_tool_decorator = self.mcp_tool_trigger(
arg_name="context",
tool_name=agent_name,
description=agent_description or f"Interact with {agent_name} agent",
tool_properties=tool_properties,
data_type=func.DataType.UNDEFINED,
)
durable_client_decorator = self.durable_client_input(client_name="client")

@function_name_decorator
@mcp_tool_decorator
@durable_client_decorator
async def mcp_tool_handler(context: str, client: df.DurableOrchestrationClient) -> str:
"""Handle MCP tool invocation for the agent.

Args:
context: MCP tool invocation context containing arguments (query, threadId)
client: Durable orchestration client for entity communication

Returns:
Agent response text
"""
logger.debug("[MCP Tool Trigger] Received invocation for agent: %s", agent_name)
return await self._handle_mcp_tool_invocation(agent_name=agent_name, context=context, client=client)

logger.debug("[AgentFunctionApp] Registered MCP tool trigger for agent: %s", agent_name)

async def _handle_mcp_tool_invocation(
self, agent_name: str, context: str | dict[str, Any], client: df.DurableOrchestrationClient
) -> str:
"""Handle an MCP tool invocation.

This method processes MCP tool requests and delegates to the agent entity.

Args:
agent_name: Name of the agent being invoked
context: MCP tool invocation context, either as a JSON string or pre-parsed dict
client: Durable orchestration client

Returns:
Agent response text

Raises:
ValueError: If required arguments are missing or context is invalid JSON
RuntimeError: If agent execution fails
"""
logger.debug("[MCP Tool Handler] Processing invocation for agent '%s'", agent_name)

# Parse context if it's a JSON string
if isinstance(context, str):
try:
parsed_context = json.loads(context)
except json.JSONDecodeError as e:
raise ValueError(f"Invalid MCP context format: {e}") from e
else:
parsed_context = context

# Extract arguments from MCP context
arguments = parsed_context.get("arguments", {}) if isinstance(parsed_context, dict) else {}

# Validate required 'query' argument
query = arguments.get("query")
if not query or not isinstance(query, str):
raise ValueError("MCP Tool invocation is missing required 'query' argument of type string.")

# Extract optional threadId
thread_id = arguments.get("threadId")

# Create or parse session ID
if thread_id and isinstance(thread_id, str) and thread_id.strip():
try:
session_id = AgentSessionId.parse(thread_id)
except ValueError as e:
logger.warning(
"Failed to parse AgentSessionId from thread_id '%s': %s. Falling back to new session ID.",
thread_id,
e,
)
session_id = AgentSessionId(name=agent_name, key=thread_id)
else:
# Generate new session ID
session_id = AgentSessionId.with_random_key(agent_name)

# Build entity instance ID
entity_instance_id = session_id.to_entity_id()

# Create run request
correlation_id = self._generate_unique_id()
run_request = self._build_request_data(
req_body={"message": query, "role": "user"},
message=query,
thread_id=str(session_id),
correlation_id=correlation_id,
request_response_format=REQUEST_RESPONSE_FORMAT_TEXT,
)

query_preview = query[:50] + "..." if len(query) > 50 else query
logger.info("[MCP Tool] Invoking agent '%s' with query: %s", agent_name, query_preview)

# Signal entity to run agent
await client.signal_entity(entity_instance_id, "run_agent", run_request)

# Poll for response (similar to HTTP handler)
try:
result = await self._get_response_from_entity(
client=client,
entity_instance_id=entity_instance_id,
correlation_id=correlation_id,
message=query,
thread_id=str(session_id),
)

# Extract and return response text
if result.get("status") == "success":
response_text = str(result.get("response", "No response"))
logger.info("[MCP Tool] Agent '%s' responded successfully", agent_name)
return response_text
error_msg = result.get("error", "Unknown error")
logger.error("[MCP Tool] Agent '%s' execution failed: %s", agent_name, error_msg)
raise RuntimeError(f"Agent execution failed: {error_msg}")

except Exception as exc:
logger.error("[MCP Tool] Error invoking agent '%s': %s", agent_name, exc, exc_info=True)
raise

def _setup_health_route(self) -> None:
"""Register the optional health check route."""
health_route = self.route(route="health", methods=["GET"])
Expand All @@ -463,6 +652,7 @@ def health_check(req: func.HttpRequest) -> func.HttpResponse:
name,
self.enable_http_endpoints,
),
"mcp_tool_enabled": self.agent_mcp_tool_flags.get(name, self.enable_mcp_tool_endpoint),
}
for name, agent in self.agents.items()
]
Expand Down
Loading
Loading