diff --git a/python/packages/azurefunctions/agent_framework_azurefunctions/_app.py b/python/packages/azurefunctions/agent_framework_azurefunctions/_app.py index 2187f6c031..11e28ddcae 100644 --- a/python/packages/azurefunctions/agent_framework_azurefunctions/_app.py +++ b/python/packages/azurefunctions/agent_framework_azurefunctions/_app.py @@ -9,7 +9,7 @@ import json import re from collections.abc import Mapping -from typing import Any, cast +from typing import TYPE_CHECKING, Any, cast import azure.durable_functions as df import azure.functions as func @@ -70,12 +70,16 @@ class AgentFunctionApp(df.DFApp): This creates: - HTTP trigger endpoint for each agent's requests (if enabled) - Durable entity for each agent's state management and execution + - MCP tool trigger for agents (if enabled) - Full access to all Azure Functions capabilities Attributes: 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_http_endpoints: Whether HTTP endpoints are created for agents by default + enable_mcp_tool_triggers: Whether MCP tool triggers are created for agents by default + agent_http_endpoint_flags: Per-agent HTTP endpoint settings + agent_mcp_tool_flags: Per-agent MCP tool trigger settings max_poll_retries: Maximum polling attempts when waiting for responses poll_interval_seconds: Delay (seconds) between polling attempts """ @@ -83,7 +87,9 @@ class AgentFunctionApp(df.DFApp): agents: dict[str, AgentProtocol] enable_health_check: bool enable_http_endpoints: bool + enable_mcp_tool_triggers: bool agent_http_endpoint_flags: dict[str, bool] + agent_mcp_tool_flags: dict[str, bool] def __init__( self, @@ -91,6 +97,7 @@ def __init__( http_auth_level: func.AuthLevel = func.AuthLevel.ANONYMOUS, enable_health_check: bool = True, enable_http_endpoints: bool = True, + enable_mcp_tool_triggers: bool = False, max_poll_retries: int = 10, poll_interval_seconds: float = 0.5, default_callback: AgentResponseCallbackProtocol | None = None, @@ -101,13 +108,15 @@ def __init__( agents: List of agent instances to register http_auth_level: HTTP authentication level (default: ANONYMOUS) enable_health_check: Enable built-in health check endpoint (default: True) - enable_http_endpoints: Enable HTTP endpoints for agents (default: True) + enable_http_endpoints: Enable HTTP endpoints for agents by default (default: True) + enable_mcp_tool_triggers: Enable MCP tool triggers for agents by default (default: False) max_poll_retries: Maximum number of polling attempts when waiting for a response poll_interval_seconds: Delay (in seconds) between polling attempts default_callback: Optional callback invoked for agents without specific callbacks Note: If no agents are provided, they can be added later using add_agent(). + Per-agent settings in add_agent() override these global defaults. """ logger.debug("[AgentFunctionApp] Initializing with Durable Entities...") @@ -116,9 +125,11 @@ def __init__( # Initialize agents dictionary self.agents = {} - self.agent_http_endpoint_flags = {} self.enable_health_check = enable_health_check self.enable_http_endpoints = enable_http_endpoints + self.enable_mcp_tool_triggers = enable_mcp_tool_triggers + self.agent_http_endpoint_flags = {} + self.agent_mcp_tool_flags = {} self.default_callback = default_callback try: @@ -150,6 +161,7 @@ def add_agent( agent: AgentProtocol, callback: AgentResponseCallbackProtocol | None = None, enable_http_endpoint: bool | None = None, + enable_mcp_tool_trigger: bool | None = None, ) -> None: """Add an agent to the function app after initialization. @@ -157,8 +169,10 @@ def add_agent( 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_trigger: Optional flag to enable/disable MCP tool trigger for this agent. + If None, uses the app-level enable_mcp_tool_triggers setting. Raises: ValueError: If the agent doesn't have a 'name' attribute or if an agent @@ -172,29 +186,27 @@ def add_agent( if name in self.agents: raise ValueError(f"Agent with name '{name}' is already registered. Each agent must have a unique name.") - effective_enable_http_endpoint = ( + # Resolve effective settings (per-agent overrides global) + effective_enable_http = ( self.enable_http_endpoints if enable_http_endpoint is None else self._coerce_to_bool(enable_http_endpoint) ) + effective_enable_mcp = ( + self.enable_mcp_tool_triggers + if enable_mcp_tool_trigger is None + else self._coerce_to_bool(enable_mcp_tool_trigger) + ) logger.debug(f"[AgentFunctionApp] Adding agent: {name}") - logger.debug(f"[AgentFunctionApp] Route: /api/agents/{name}") - logger.debug( - "[AgentFunctionApp] HTTP endpoint %s for agent '%s'", - "enabled" if effective_enable_http_endpoint else "disabled", - name, - ) + logger.debug(f"[AgentFunctionApp] HTTP endpoint: {'enabled' if effective_enable_http else 'disabled'}") + logger.debug(f"[AgentFunctionApp] MCP tool trigger: {'enabled' if effective_enable_mcp else 'disabled'}") self.agents[name] = agent - self.agent_http_endpoint_flags[name] = effective_enable_http_endpoint + self.agent_http_endpoint_flags[name] = effective_enable_http + self.agent_mcp_tool_flags[name] = effective_enable_mcp effective_callback = callback or self.default_callback - self._setup_agent_functions( - agent, - name, - effective_callback, - effective_enable_http_endpoint, - ) + self._setup_agent_functions(agent, name, effective_callback, effective_enable_http, effective_enable_mcp) logger.debug(f"[AgentFunctionApp] Agent '{name}' added successfully") @@ -204,27 +216,36 @@ def _setup_agent_functions( agent_name: str, callback: AgentResponseCallbackProtocol | None, enable_http_endpoint: bool, + enable_mcp_tool_trigger: 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_trigger: Whether to create MCP tool trigger """ logger.debug(f"[AgentFunctionApp] Setting up functions for agent '{agent_name}'...") + # Set up HTTP endpoints if enabled if enable_http_endpoint: self._setup_http_run_route(agent_name) + self._setup_get_state_route(agent_name) else: - logger.debug( - "[AgentFunctionApp] HTTP run route disabled for agent '%s'", - agent_name, - ) + logger.debug(f"[AgentFunctionApp] HTTP endpoints disabled for agent '{agent_name}'") + + # Always set up entity (for state management) self._setup_agent_entity(agent, agent_name, callback) + # Set up MCP tool trigger if enabled + if enable_mcp_tool_trigger: + 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. @@ -355,6 +376,197 @@ 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_get_state_route(self, agent_name: str) -> None: + """Register the GET route for retrieving conversation state. + + Args: + agent_name: The agent name (used for both routing and entity identification) + """ + state_function_name = self._build_function_name(agent_name, "state") + + @self.function_name(state_function_name) + @self.route( + route=f"agents/{agent_name}/{{{SESSION_ID_FIELD}}}", + methods=["GET"], + ) + @self.durable_client_input(client_name="client") + async def get_conversation_state( + req: func.HttpRequest, client: df.DurableOrchestrationClient + ) -> func.HttpResponse: + """GET endpoint to retrieve conversation state for a given sessionId.""" + session_key = req.route_params.get(SESSION_ID_FIELD) + + logger.debug(f"[GET State] Retrieving state for session: {session_key}") + + try: + session_id = AgentSessionId(name=agent_name, key=session_key) + entity_instance_id = session_id.to_entity_id() + + state_response = await client.read_entity_state(entity_instance_id) + + if not state_response or not state_response.entity_exists: + logger.warning(f"[GET State] Session not found: {session_key}") + return func.HttpResponse( + json.dumps({"error": "Session not found"}), + status_code=404, + mimetype="application/json", + ) + + state = state_response.entity_state + if isinstance(state, str): + state = json.loads(state) if state else {} + + logger.debug(f"[GET State] Found conversation with {state.get('message_count', 0)} messages") + + return func.HttpResponse(json.dumps(state, indent=2), status_code=200, mimetype="application/json") + + except Exception as exc: + logger.error(f"[GET State] Error: {str(exc)}", exc_info=True) + return func.HttpResponse( + json.dumps({"error": str(exc)}), status_code=500, mimetype="application/json" + ) + + 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) + """ + # Match .NET naming convention: mcptool-{agentName} + mcp_function_name = f"mcptool-{agent_name}" + + # 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, + }, + ] + ) + + @self.function_name(mcp_function_name) + @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, + ) + @self.durable_client_input(client_name="client") + async def mcp_tool_handler(context: str, client: df.DurableOrchestrationClient) -> str: + """Handle MCP tool invocation for the agent.""" + return await self._handle_mcp_tool_invocation(agent_name=agent_name, context=context, client=client) + + logger.info(f"[AgentFunctionApp] Registered MCP tool trigger: {agent_name}") + + async def _handle_mcp_tool_invocation( + self, agent_name: str, context: 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 containing arguments + client: Durable orchestration client + + Returns: + Agent response text + + Raises: + ValueError: If required arguments are missing + RuntimeError: If agent execution fails + """ + # Parse context if it's a JSON string + if isinstance(context, str): + try: + context = json.loads(context) + except json.JSONDecodeError as e: + raise ValueError(f"Invalid MCP context format: {e}") + + # Extract arguments from MCP context + arguments = context.get("arguments", {}) if isinstance(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 Exception: + # If parsing fails, create new session ID with thread_id as key + 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, + conversation_id=str(session_id), + correlation_id=correlation_id, + ) + + logger.info( + f"[MCP Tool] Invoking agent '{agent_name}' with query: {query[:50]}..." + ("" if len(query) <= 50 else "") + ) + + # 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, + session_key=str(session_id), + ) + + # Extract and return response text + if result.get("status") == "success": + response_text = result.get("response", "No response") + logger.info(f"[MCP Tool] Agent '{agent_name}' responded successfully") + return response_text + else: + error_msg = result.get("error", "Unknown error") + logger.error(f"[MCP Tool] Agent '{agent_name}' execution failed: {error_msg}") + raise RuntimeError(f"Agent execution failed: {error_msg}") + + except Exception as exc: + logger.error(f"[MCP Tool] Error invoking agent '{agent_name}': {str(exc)}", exc_info=True) + raise + def _setup_health_route(self) -> None: """Register the optional health check route.""" @@ -365,10 +577,8 @@ def health_check(req: func.HttpRequest) -> func.HttpResponse: { "name": name, "type": type(agent).__name__, - "httpEndpointEnabled": self.agent_http_endpoint_flags.get( - name, - self.enable_http_endpoints, - ), + "httpEndpointEnabled": self.agent_http_endpoint_flags.get(name, self.enable_http_endpoints), + "mcpToolTriggerEnabled": self.agent_mcp_tool_flags.get(name, self.enable_mcp_tool_triggers), } for name, agent in self.agents.items() ] @@ -393,25 +603,6 @@ def _build_function_name(agent_name: str, suffix: str) -> str: return f"{sanitized}_{suffix}" - async def _read_cached_state( - self, - client: df.DurableOrchestrationClient, - entity_instance_id: df.EntityId, - ) -> AgentState | None: - state_response = await client.read_entity_state(entity_instance_id) - if not state_response or not state_response.entity_exists: - return None - - state_payload = state_response.entity_state - if not isinstance(state_payload, dict): - return None - - typed_state_payload = cast(dict[str, Any], state_payload) - - agent_state = AgentState() - agent_state.restore_state(typed_state_payload) - return agent_state - async def _get_response_from_entity( self, client: df.DurableOrchestrationClient, @@ -507,6 +698,7 @@ def _build_success_result( "status": "success", "message_count": response_data.get("message_count", state.message_count), "correlationId": correlation_id, + "timestamp": response_data.get("timestamp"), } def _build_request_data( @@ -644,6 +836,32 @@ def _coerce_chat_role(self, value: Any) -> ChatRole: logger.warning("[AgentFunctionApp] Invalid role '%s'; defaulting to user", value) return ChatRole.USER + async def _read_cached_state( + self, + client: df.DurableOrchestrationClient, + entity_instance_id: df.EntityId, + ) -> AgentState | None: + """Read the entity state from storage. + + Args: + client: Durable orchestration client + entity_instance_id: Entity ID to read + + Returns: + AgentState if entity exists, None otherwise + """ + state_response = await client.read_entity_state(entity_instance_id) + if not state_response or not state_response.entity_exists: + return None + + state_payload = state_response.entity_state + if not isinstance(state_payload, dict): + return None + + agent_state = AgentState() + agent_state.restore_state(state_payload) + return agent_state + def _coerce_to_bool(self, value: Any) -> bool: """Convert various representations into a boolean flag.""" if isinstance(value, bool): diff --git a/python/packages/azurefunctions/agent_framework_azurefunctions/_state.py b/python/packages/azurefunctions/agent_framework_azurefunctions/_state.py index c15d9fe96d..0f54cd8a37 100644 --- a/python/packages/azurefunctions/agent_framework_azurefunctions/_state.py +++ b/python/packages/azurefunctions/agent_framework_azurefunctions/_state.py @@ -89,8 +89,22 @@ def add_assistant_message( ) def get_chat_messages(self) -> list[ChatMessage]: - """Return a copy of the full conversation history.""" - return list(self.conversation_history) + """Return a copy of the full conversation history. + + Note: additional_properties are stripped from the returned messages to avoid + sending metadata to the LLM API, which doesn't support it in the messages array. + The original messages in conversation_history retain their additional_properties. + """ + # Return messages without additional_properties to avoid API errors + # Azure OpenAI doesn't support metadata in the messages array + return [ + ChatMessage( + role=msg.role, + contents=msg.contents, + # Intentionally omit additional_properties + ) + for msg in self.conversation_history + ] def try_get_agent_response(self, correlation_id: str) -> dict[str, Any] | None: """Get an agent response by correlation ID. diff --git a/python/samples/getting_started/azure_functions/08_mcp_server/README.md b/python/samples/getting_started/azure_functions/08_mcp_server/README.md new file mode 100644 index 0000000000..53ed11b7c0 --- /dev/null +++ b/python/samples/getting_started/azure_functions/08_mcp_server/README.md @@ -0,0 +1,188 @@ +# Agent as MCP Tool Sample + +This sample demonstrates how to configure AI agents to be accessible as both HTTP endpoints and [Model Context Protocol (MCP)](https://modelcontextprotocol.io/) tools, enabling flexible integration patterns for AI agent consumption. + +## Key Concepts Demonstrated + +- **Multi-trigger Agent Configuration**: Configure agents to support HTTP triggers, MCP tool triggers, or both +- **Microsoft Agent Framework Integration**: Use the framework to define AI agents with specific roles and capabilities +- **Flexible Agent Registration**: Register agents with customizable trigger configurations +- **MCP Server Hosting**: Expose agents as MCP tools for consumption by MCP-compatible clients + +## Sample Architecture + +This sample creates three agents with different trigger configurations: + +| Agent | Role | HTTP Trigger | MCP Tool Trigger | Description | +|-------|------|--------------|------------------|-------------| +| **Joker** | Comedy specialist | ✅ Enabled | ❌ Disabled | Accessible only via HTTP requests | +| **StockAdvisor** | Financial data | ❌ Disabled | ✅ Enabled | Accessible only as MCP tool | +| **PlantAdvisor** | Indoor plant recommendations | ✅ Enabled | ✅ Enabled | Accessible via both HTTP and MCP | + +## Environment Setup + +See the [README.md](../README.md) file in the parent directory for complete setup instructions, including: + +- Prerequisites installation +- Azure OpenAI configuration +- Durable Task Scheduler setup +- Storage emulator configuration + +## Configuration + +Update your `local.settings.json` with your Azure OpenAI credentials: + +```json +{ + "Values": { + "AZURE_OPENAI_ENDPOINT": "https://your-resource.openai.azure.com/", + "AZURE_OPENAI_CHAT_DEPLOYMENT_NAME": "your-deployment-name", + "AZURE_OPENAI_KEY": "your-api-key-if-not-using-rbac" + } +} +``` + +## Running the Sample + +1. **Start the Function App**: + ```bash + cd python/samples/getting_started/azure_functions/08_mcp_server + func start + ``` + +2. **Note the MCP Server Endpoint**: When the app starts, you'll see the MCP server endpoint in the terminal output. It will look like: + ``` + MCP server endpoint: http://localhost:7071/runtime/webhooks/mcp + ``` + +## Testing MCP Tool Integration + +### Using MCP Inspector + +1. Install the [MCP Inspector](https://modelcontextprotocol.io/docs/tools/inspector) +2. Connect using the MCP server endpoint from your terminal output +3. Select **"Streamable HTTP"** as the transport method +4. Test the available MCP tools: + - `StockAdvisor` - Available only as MCP tool + - `PlantAdvisor` - Available as both HTTP and MCP tool + +### Using Other MCP Clients + +Any MCP-compatible client can connect to the server endpoint and utilize the exposed agent tools. The agents will appear as callable tools within the MCP protocol. + +## Testing HTTP Endpoints + +For agents with HTTP triggers enabled (Joker and PlantAdvisor), you can test them using curl: + +```bash +# Test Joker agent (HTTP only) +curl -X POST http://localhost:7071/api/agents/Joker/run \ + -H "Content-Type: application/json" \ + -d '{"message": "Tell me a joke"}' + +# Test PlantAdvisor agent (HTTP and MCP) +curl -X POST http://localhost:7071/api/agents/PlantAdvisor/run \ + -H "Content-Type: application/json" \ + -d '{"message": "Recommend an indoor plant"}' +``` + +Note: StockAdvisor does not have HTTP endpoints and is only accessible via MCP tool triggers. + +## Expected Output + +**HTTP Responses** will be returned directly to your HTTP client. + +**MCP Tool Responses** will be visible in: +- The terminal where `func start` is running +- Your MCP client interface +- The DTS dashboard at `http://localhost:8080` (if using Durable Task Scheduler) + +## Health Check + +Check the health endpoint to see which agents have which triggers enabled: + +```bash +curl http://localhost:7071/api/health +``` + +Expected response: + +```json +{ + "status": "healthy", + "agents": [ + { + "name": "Joker", + "type": "Agent", + "httpEndpointEnabled": true, + "mcpToolTriggerEnabled": false + }, + { + "name": "StockAdvisor", + "type": "Agent", + "httpEndpointEnabled": false, + "mcpToolTriggerEnabled": true + }, + { + "name": "PlantAdvisor", + "type": "Agent", + "httpEndpointEnabled": true, + "mcpToolTriggerEnabled": true + } + ], + "agent_count": 3 +} +``` + +## Code Structure + +The sample shows how to enable MCP tool triggers with flexible agent configuration: + +```python +from agent_framework.azurefunctions import AgentFunctionApp +from agent_framework.azure import AzureOpenAIChatClient + +# Create Azure OpenAI Chat Client +chat_client = AzureOpenAIChatClient() + +# Define agents with different roles +joker_agent = chat_client.create_agent( + name="Joker", + instructions="You are good at telling jokes.", +) + +stock_agent = chat_client.create_agent( + name="StockAdvisor", + instructions="Check stock prices.", +) + +plant_agent = chat_client.create_agent( + name="PlantAdvisor", + instructions="Recommend plants.", + description="Get plant recommendations.", +) + +# Create the AgentFunctionApp +app = AgentFunctionApp(enable_health_check=True) + +# Configure agents with different trigger combinations: +# HTTP trigger only (default) +app.add_agent(joker_agent) + +# MCP tool trigger only (HTTP disabled) +app.add_agent(stock_agent, enable_http_endpoint=False, enable_mcp_tool_trigger=True) + +# Both HTTP and MCP tool triggers enabled +app.add_agent(plant_agent, enable_http_endpoint=True, enable_mcp_tool_trigger=True) +``` + +This automatically creates the following endpoints based on agent configuration: +- `POST /api/agents/{AgentName}/run` - HTTP endpoint (when `enable_http_endpoint=True`) +- MCP tool triggers for agents with `enable_mcp_tool_trigger=True` +- `GET /api/health` - Health check endpoint showing agent configurations + +## Learn More + +- [Model Context Protocol Documentation](https://modelcontextprotocol.io/) +- [Microsoft Agent Framework Documentation](https://github.com/Azure/agent-framework) +- [Azure Functions Documentation](https://learn.microsoft.com/azure/azure-functions/) diff --git a/python/samples/getting_started/azure_functions/08_mcp_server/function_app.py b/python/samples/getting_started/azure_functions/08_mcp_server/function_app.py new file mode 100644 index 0000000000..0b157bee56 --- /dev/null +++ b/python/samples/getting_started/azure_functions/08_mcp_server/function_app.py @@ -0,0 +1,64 @@ +""" +Example showing how to configure AI agents with different trigger configurations. + +This sample demonstrates how to configure agents to be accessible as both HTTP endpoints +and Model Context Protocol (MCP) tools, enabling flexible integration patterns for AI agent +consumption. + +Key concepts demonstrated: +- Multi-trigger Agent Configuration: Configure agents to support HTTP triggers, MCP tool triggers, or both +- Microsoft Agent Framework Integration: Use the framework to define AI agents with specific roles +- Flexible Agent Registration: Register agents with customizable trigger configurations + +This sample creates three agents with different trigger configurations: +- Joker: HTTP trigger only (default) +- StockAdvisor: MCP tool trigger only (HTTP disabled) +- PlantAdvisor: Both HTTP and MCP tool triggers enabled + +Required environment variables: +- AZURE_OPENAI_ENDPOINT: Your Azure OpenAI endpoint +- AZURE_OPENAI_CHAT_DEPLOYMENT_NAME: Your Azure OpenAI deployment name + +Authentication uses AzureCliCredential (Azure Identity). +""" + +from agent_framework.azurefunctions import AgentFunctionApp +from agent_framework.azure import AzureOpenAIChatClient + +# Create Azure OpenAI Chat Client +# This uses AzureCliCredential for authentication (requires 'az login') +chat_client = AzureOpenAIChatClient() + +# Define three AI agents with different roles +# Agent 1: Joker - HTTP trigger only (default) +agent1 = chat_client.create_agent( + name="Joker", + instructions="You are good at telling jokes.", +) + +# Agent 2: StockAdvisor - MCP tool trigger only +agent2 = chat_client.create_agent( + name="StockAdvisor", + instructions="Check stock prices.", +) + +# Agent 3: PlantAdvisor - Both HTTP and MCP tool triggers +agent3 = chat_client.create_agent( + name="PlantAdvisor", + instructions="Recommend plants.", + description="Get plant recommendations.", +) + +# Create the AgentFunctionApp with selective trigger configuration +app = AgentFunctionApp( + enable_health_check=True, +) + +# Agent 1: HTTP trigger only (default) +app.add_agent(agent1) + +# Agent 2: Disable HTTP trigger, enable MCP tool trigger only +app.add_agent(agent2, enable_http_endpoint=False, enable_mcp_tool_trigger=True) + +# Agent 3: Enable both HTTP and MCP tool triggers +app.add_agent(agent3, enable_http_endpoint=True, enable_mcp_tool_trigger=True) diff --git a/python/samples/getting_started/azure_functions/08_mcp_server/host.json b/python/samples/getting_started/azure_functions/08_mcp_server/host.json new file mode 100644 index 0000000000..b7e5ad1c0b --- /dev/null +++ b/python/samples/getting_started/azure_functions/08_mcp_server/host.json @@ -0,0 +1,7 @@ +{ + "version": "2.0", + "extensionBundle": { + "id": "Microsoft.Azure.Functions.ExtensionBundle", + "version": "[4.*, 5.0.0)" + } +} diff --git a/python/samples/getting_started/azure_functions/08_mcp_server/local.settings.json.template b/python/samples/getting_started/azure_functions/08_mcp_server/local.settings.json.template new file mode 100644 index 0000000000..6c98a7d1cb --- /dev/null +++ b/python/samples/getting_started/azure_functions/08_mcp_server/local.settings.json.template @@ -0,0 +1,10 @@ +{ + "IsEncrypted": false, + "Values": { + "FUNCTIONS_WORKER_RUNTIME": "python", + "AzureWebJobsStorage": "UseDevelopmentStorage=true", + "DURABLE_TASK_SCHEDULER_CONNECTION_STRING": "Endpoint=http://localhost:8080;TaskHub=default;Authentication=None", + "AZURE_OPENAI_ENDPOINT": "", + "AZURE_OPENAI_CHAT_DEPLOYMENT_NAME": "" + } +} diff --git a/python/samples/getting_started/azure_functions/08_mcp_server/requirements.txt b/python/samples/getting_started/azure_functions/08_mcp_server/requirements.txt new file mode 100644 index 0000000000..39ad8a124f --- /dev/null +++ b/python/samples/getting_started/azure_functions/08_mcp_server/requirements.txt @@ -0,0 +1,2 @@ +agent-framework-azurefunctions +azure-identity