diff --git a/python/samples/04-hosting/foundry-hosted-agents/responses/12_foundry_toolbox_mcp_skills/.dockerignore b/python/samples/04-hosting/foundry-hosted-agents/responses/12_foundry_toolbox_mcp_skills/.dockerignore new file mode 100644 index 0000000000..31ed562a7e --- /dev/null +++ b/python/samples/04-hosting/foundry-hosted-agents/responses/12_foundry_toolbox_mcp_skills/.dockerignore @@ -0,0 +1,7 @@ +.venv +__pycache__ +*.pyc +*.pyo +*.pyd +.Python +.env diff --git a/python/samples/04-hosting/foundry-hosted-agents/responses/12_foundry_toolbox_mcp_skills/.env.example b/python/samples/04-hosting/foundry-hosted-agents/responses/12_foundry_toolbox_mcp_skills/.env.example new file mode 100644 index 0000000000..0fb7bb652e --- /dev/null +++ b/python/samples/04-hosting/foundry-hosted-agents/responses/12_foundry_toolbox_mcp_skills/.env.example @@ -0,0 +1,3 @@ +FOUNDRY_PROJECT_ENDPOINT="..." +AZURE_AI_MODEL_DEPLOYMENT_NAME="..." +TOOLBOX_NAME="..." diff --git a/python/samples/04-hosting/foundry-hosted-agents/responses/12_foundry_toolbox_mcp_skills/Dockerfile b/python/samples/04-hosting/foundry-hosted-agents/responses/12_foundry_toolbox_mcp_skills/Dockerfile new file mode 100644 index 0000000000..12c4791bc9 --- /dev/null +++ b/python/samples/04-hosting/foundry-hosted-agents/responses/12_foundry_toolbox_mcp_skills/Dockerfile @@ -0,0 +1,18 @@ +FROM python:3.12-slim + +RUN apt-get update && apt-get install -y git && rm -rf /var/lib/apt/lists/* + +WORKDIR /app + +COPY . user_agent/ +WORKDIR /app/user_agent + +RUN if [ -f requirements.txt ]; then \ + pip install -r requirements.txt; \ + else \ + echo "No requirements.txt found"; \ + fi + +EXPOSE 8088 + +CMD ["python", "main.py"] diff --git a/python/samples/04-hosting/foundry-hosted-agents/responses/12_foundry_toolbox_mcp_skills/README.md b/python/samples/04-hosting/foundry-hosted-agents/responses/12_foundry_toolbox_mcp_skills/README.md new file mode 100644 index 0000000000..5f9ae98756 --- /dev/null +++ b/python/samples/04-hosting/foundry-hosted-agents/responses/12_foundry_toolbox_mcp_skills/README.md @@ -0,0 +1,72 @@ +# What this sample demonstrates + +An [Agent Framework](https://github.com/microsoft/agent-framework) agent that discovers **MCP-based skills from a Foundry Toolbox** and makes them available via `SkillsProvider(MCPSkillsSource(...))`, hosted using the **Responses protocol**. + +The `SkillsProvider` is attached to the agent as a context provider and implements the [Agent Skills](https://agentskills.io/) progressive-disclosure pattern. When the agent is prompted, it discovers available skills in the Foundry Toolbox via the provider: + +1. **Advertise** — skill names and descriptions are injected into the system prompt so the agent knows what is available. +2. **Load** — when the agent decides a skill is relevant, it retrieves the full skill body with detailed instructions via the provider. +3. **Read resources** — if a skill includes supplementary content (reference documents, assets), the agent reads them on demand via the provider. + +This way the full skill body and resources are only loaded when the agent actually needs them, reducing token usage. + +## How It Works + +### Model Integration + +The agent uses `FoundryChatClient` from the Agent Framework to create an OpenAI-compatible Responses client. It connects to the toolbox's MCP endpoint via the `mcp` library's `streamable_http_client`, discovers skills served by the toolbox through `MCPSkillsSource`, and injects them as a context provider via `SkillsProvider`. The toolbox endpoint URL is derived from `FOUNDRY_PROJECT_ENDPOINT` and `TOOLBOX_NAME`. + +See [main.py](main.py) for the full implementation. + +### Agent Hosting + +The agent is hosted using the [Agent Framework](https://github.com/microsoft/agent-framework) with the `ResponsesHostServer`, which provisions a REST API endpoint compatible with the OpenAI Responses protocol. + +## Prerequisites + +- Python 3.12+ +- An Azure AI Foundry project with a deployed model (e.g., `gpt-5`) +- A Foundry Toolbox with skills attached (see below) +- Azure CLI logged in (`az login`) + +## Setting up a Foundry Toolbox with skills + +This sample requires a Foundry Toolbox that has skills attached to it. Skills are `SKILL.md` files you author once, store centrally in Foundry through the versioned Skills API, and attach to a toolbox so any MCP client can discover and load them. + +1. **Author a skill** — Create a `SKILL.md` file following the [Agent Skills](https://agentskills.io/) specification format (YAML front matter with `name` and `description`, plus Markdown body). +2. **Create the skill in Foundry** — Upload the skill via the Skills REST API, Python SDK, or `azd ai skill create`. See [Use skills with Microsoft Foundry agents](https://learn.microsoft.com/en-us/azure/foundry/agents/how-to/tools/skills). +3. **Attach the skill to a toolbox** — Add a skill reference to a toolbox version so MCP clients can discover it. See [Attach skills to a toolbox](https://learn.microsoft.com/en-us/azure/foundry/agents/how-to/tools/toolbox#attach-skills-to-a-toolbox). + +When the agent connects to the toolbox MCP endpoint, skills are advertised through a well-known `skill://index.json` discovery resource. The `MCPSkillsSource` in this sample reads `skill://index.json` the first time the agent runs to discover all attached skills, then fetches each `SKILL.md` body on demand via `resources/read`. + +## Running the Agent Host + +Follow the instructions in the [Running the Agent Host Locally](../../README.md#running-the-agent-host-locally) section of the README in the parent directory to run the agent host. + +An extra environment variable must be set to point to the toolbox name: + +```bash +export TOOLBOX_NAME="my-toolbox" +``` + +Or in PowerShell: + +```powershell +$env:TOOLBOX_NAME="my-toolbox" +``` + +You can also place these in a `.env` file next to `main.py` — see [`.env.example`](.env.example). + +## Interacting with the agent + +> Depending on how you run the agent host, you can invoke the agent using `curl` (`Invoke-WebRequest` in PowerShell) or `azd`. Please refer to the [parent README](../../README.md) for more details. Use this README for sample queries you can send to the agent. + +Send a POST request to the server with a JSON body containing an `"input"` field to interact with the agent. For example: + +```bash +curl -X POST http://localhost:8088/responses -H "Content-Type: application/json" -d '{"input": "What skills do you have available?"}' +``` + +## Deploying the Agent to Foundry + +To host the agent on Foundry, follow the instructions in the [Deploying the Agent to Foundry](../../README.md#deploying-the-agent-to-foundry) section of the README in the parent directory. diff --git a/python/samples/04-hosting/foundry-hosted-agents/responses/12_foundry_toolbox_mcp_skills/agent.manifest.yaml b/python/samples/04-hosting/foundry-hosted-agents/responses/12_foundry_toolbox_mcp_skills/agent.manifest.yaml new file mode 100644 index 0000000000..924efd9486 --- /dev/null +++ b/python/samples/04-hosting/foundry-hosted-agents/responses/12_foundry_toolbox_mcp_skills/agent.manifest.yaml @@ -0,0 +1,42 @@ +name: hosted-toolbox-mcp-skills +displayName: "Hosted Toolbox MCP Skills Agent" + +description: > + A hosted agent that discovers MCP-based skills from a Foundry Toolbox + and makes them available to the agent via the agent skills provider. + +metadata: + tags: + - AI Agent Hosting + - Azure AI AgentServer + - Responses Protocol + - Agent Framework + - MCP + - Model Context Protocol + - Agent Skills + - Foundry Toolbox + - Foundry Toolbox Skills + +template: + name: hosted-toolbox-mcp-skills + kind: hosted + protocols: + - protocol: responses + version: 1.0.0 + resources: + cpu: "0.25" + memory: 0.5Gi + environment_variables: + - name: AZURE_AI_MODEL_DEPLOYMENT_NAME + value: "{{AZURE_AI_MODEL_DEPLOYMENT_NAME}}" + - name: TOOLBOX_NAME + value: "{{TOOLBOX_NAME}}" +parameters: + properties: + - name: TOOLBOX_NAME + secret: false + description: Name of the Foundry Toolbox to connect to for MCP skill discovery +resources: + - kind: model + id: gpt-5 + name: AZURE_AI_MODEL_DEPLOYMENT_NAME diff --git a/python/samples/04-hosting/foundry-hosted-agents/responses/12_foundry_toolbox_mcp_skills/agent.yaml b/python/samples/04-hosting/foundry-hosted-agents/responses/12_foundry_toolbox_mcp_skills/agent.yaml new file mode 100644 index 0000000000..c3167460a7 --- /dev/null +++ b/python/samples/04-hosting/foundry-hosted-agents/responses/12_foundry_toolbox_mcp_skills/agent.yaml @@ -0,0 +1,14 @@ +# yaml-language-server: $schema=https://raw.githubusercontent.com/microsoft/AgentSchema/refs/heads/main/schemas/v1.0/ContainerAgent.yaml +kind: hosted +name: hosted-toolbox-mcp-skills +protocols: + - protocol: responses + version: 1.0.0 +resources: + cpu: "0.25" + memory: 0.5Gi +environment_variables: + - name: AZURE_AI_MODEL_DEPLOYMENT_NAME + value: ${AZURE_AI_MODEL_DEPLOYMENT_NAME} + - name: TOOLBOX_NAME + value: ${TOOLBOX_NAME} diff --git a/python/samples/04-hosting/foundry-hosted-agents/responses/12_foundry_toolbox_mcp_skills/main.py b/python/samples/04-hosting/foundry-hosted-agents/responses/12_foundry_toolbox_mcp_skills/main.py new file mode 100644 index 0000000000..180d4d941c --- /dev/null +++ b/python/samples/04-hosting/foundry-hosted-agents/responses/12_foundry_toolbox_mcp_skills/main.py @@ -0,0 +1,94 @@ +# Copyright (c) Microsoft. All rights reserved. + +import asyncio +import os +from collections.abc import Callable, Generator + +import httpx +from agent_framework import Agent, MCPSkillsSource, SkillsProvider +from agent_framework.foundry import FoundryChatClient +from agent_framework_foundry_hosting import ResponsesHostServer +from azure.identity import DefaultAzureCredential, get_bearer_token_provider +from dotenv import load_dotenv +from mcp.client.session import ClientSession +from mcp.client.streamable_http import streamable_http_client + +# Load environment variables from .env file +load_dotenv() + + +class ToolboxAuth(httpx.Auth): + """Attach a fresh Foundry bearer token to every request.""" + + def __init__(self, token_provider: Callable[[], str]): + self._get_token = token_provider + + def auth_flow(self, request: httpx.Request) -> Generator[httpx.Request, httpx.Response, None]: + request.headers["Authorization"] = f"Bearer {self._get_token()}" + yield request + + +async def main() -> None: + project_endpoint = os.environ["FOUNDRY_PROJECT_ENDPOINT"] + deployment = os.environ["AZURE_AI_MODEL_DEPLOYMENT_NAME"] + toolbox_name = os.environ["TOOLBOX_NAME"] + + # Build the Toolbox MCP URL from the project endpoint and toolbox name. + toolbox_mcp_url = f"{project_endpoint.rstrip('/')}/toolboxes/{toolbox_name}/mcp?api-version=v1" + + credential = DefaultAzureCredential() + + # Create a token provider for Foundry bearer auth + token_provider = get_bearer_token_provider(credential, "https://ai.azure.com/.default") + + # ── Connect to the Foundry Toolbox MCP endpoint ────────────────────────── + # Create an HTTP client that attaches a fresh Foundry bearer token to every + # request and advertises the toolbox preview feature flag. + async with ( + httpx.AsyncClient( + auth=ToolboxAuth(token_provider), + headers={"Foundry-Features": "Toolboxes=V1Preview"}, + timeout=httpx.Timeout(30.0, read=300.0), + follow_redirects=True, + ) as http_client, + streamable_http_client( + url=toolbox_mcp_url, + http_client=http_client, + ) as (read, write, _), + ClientSession(read, write) as session, + ): + await session.initialize() + + print(f"Connected to Foundry Toolbox '{toolbox_name}' MCP server.") + + # ── Configure MCP-based skills provider ────────────────────────────── + # MCPSkillsSource reads skill://index.json and creates one MCPSkill per + # skill-md entry; SKILL.md bodies are fetched on demand via + # resources/read. + skills_provider = SkillsProvider(MCPSkillsSource(client=session)) + + # ── Create the agent ───────────────────────────────────────────────── + client = FoundryChatClient( + project_endpoint=project_endpoint, + model=deployment, + credential=credential, + ) + + agent = Agent( + client=client, + name=os.environ.get("AGENT_NAME", "hosted-toolbox-mcp-skills"), + instructions="You are a helpful assistant.", + context_providers=[skills_provider], + # History will be managed by the hosting infrastructure, thus there + # is no need to store history by the service. Learn more at: + # https://developers.openai.com/api/reference/resources/responses/methods/create + default_options={"store": False}, + ) + + # ── Build and run the host ─────────────────────────────────────────── + server = ResponsesHostServer(agent) + await server.run_async() + + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/python/samples/04-hosting/foundry-hosted-agents/responses/12_foundry_toolbox_mcp_skills/requirements.txt b/python/samples/04-hosting/foundry-hosted-agents/responses/12_foundry_toolbox_mcp_skills/requirements.txt new file mode 100644 index 0000000000..96c42b5355 --- /dev/null +++ b/python/samples/04-hosting/foundry-hosted-agents/responses/12_foundry_toolbox_mcp_skills/requirements.txt @@ -0,0 +1,4 @@ +agent-framework +agent-framework-foundry-hosting + +mcp>=1.24.0,<2