diff --git a/python/packages/foundry_hosting/agent_framework_foundry_hosting/_toolbox.py b/python/packages/foundry_hosting/agent_framework_foundry_hosting/_toolbox.py index 28b87593415..a3848a75bed 100644 --- a/python/packages/foundry_hosting/agent_framework_foundry_hosting/_toolbox.py +++ b/python/packages/foundry_hosting/agent_framework_foundry_hosting/_toolbox.py @@ -195,6 +195,9 @@ def as_skills_provider( source_id: str | None = None, instruction_template: str | None = None, disable_caching: bool = False, + disable_load_skill_approval: bool = False, + disable_read_skill_resource_approval: bool = False, + disable_run_skill_script_approval: bool = False, ) -> SkillsProvider: """Return a :class:`~agent_framework.SkillsProvider` backed by this toolbox. @@ -214,6 +217,18 @@ def as_skills_provider( skills; see :class:`~agent_framework.SkillsProvider`. disable_caching: Re-query the toolbox on every agent run instead of caching after the first discovery. + disable_load_skill_approval: When ``True``, register the provider's + ``load_skill`` tool with ``approval_mode="never_require"`` so loading + a skill body needs no host approval. Set this for unattended agents + (for example, an agent hosted behind :class:`ResponsesHostServer`, + which runs without an :class:`~agent_framework.AgentSession` and so + cannot satisfy the default approval flow). Defaults to ``False``. + disable_read_skill_resource_approval: When ``True``, register the + provider's ``read_skill_resource`` tool with + ``approval_mode="never_require"``. Defaults to ``False``. + disable_run_skill_script_approval: When ``True``, register the provider's + ``run_skill_script`` tool with ``approval_mode="never_require"``. + Defaults to ``False``. Returns: A :class:`~agent_framework.SkillsProvider` that advertises and loads the @@ -228,7 +243,9 @@ def as_skills_provider( # ``tools=toolbox`` connects the MCP session; ``load_tools=False`` # keeps its tools hidden so only its skills are surfaced. tools=toolbox, - context_providers=[toolbox.as_skills_provider()], + # ``disable_load_skill_approval`` lets the hosted agent load + # skills without an approval round-trip (no AgentSession needed). + context_providers=[toolbox.as_skills_provider(disable_load_skill_approval=True)], default_options={"store": False}, ) await ResponsesHostServer(agent).run_async() @@ -238,6 +255,9 @@ def as_skills_provider( source_id=source_id, instruction_template=instruction_template, disable_caching=disable_caching, + disable_load_skill_approval=disable_load_skill_approval, + disable_read_skill_resource_approval=disable_read_skill_resource_approval, + disable_run_skill_script_approval=disable_run_skill_script_approval, ) diff --git a/python/packages/foundry_hosting/tests/test_toolbox.py b/python/packages/foundry_hosting/tests/test_toolbox.py index 6aa7bf26e6e..005eefa5766 100644 --- a/python/packages/foundry_hosting/tests/test_toolbox.py +++ b/python/packages/foundry_hosting/tests/test_toolbox.py @@ -165,6 +165,35 @@ def test_as_skills_provider_returns_provider() -> None: assert provider.source_id == "toolbox-skills" +def test_as_skills_provider_requires_approval_by_default() -> None: + toolbox = FoundryToolbox( + _FakeCredential(), # type: ignore + url="https://h/toolboxes/tb/mcp", + ) + provider = toolbox.as_skills_provider() + # By default every skill tool keeps its approval requirement. + assert provider._disable_load_skill_approval is False # pyright: ignore[reportPrivateUsage] + assert provider._disable_read_skill_resource_approval is False # pyright: ignore[reportPrivateUsage] + assert provider._disable_run_skill_script_approval is False # pyright: ignore[reportPrivateUsage] + + +def test_as_skills_provider_forwards_approval_overrides() -> None: + toolbox = FoundryToolbox( + _FakeCredential(), # type: ignore + url="https://h/toolboxes/tb/mcp", + ) + provider = toolbox.as_skills_provider( + disable_load_skill_approval=True, + disable_read_skill_resource_approval=True, + disable_run_skill_script_approval=True, + ) + # Overrides flow through to the underlying SkillsProvider so an unattended + # host (no AgentSession) can load skills without an approval round-trip. + assert provider._disable_load_skill_approval is True # pyright: ignore[reportPrivateUsage] + assert provider._disable_read_skill_resource_approval is True # pyright: ignore[reportPrivateUsage] + assert provider._disable_run_skill_script_approval is True # pyright: ignore[reportPrivateUsage] + + async def test_skills_source_requires_connection() -> None: toolbox = FoundryToolbox( _FakeCredential(), # type: ignore diff --git a/python/samples/04-hosting/foundry-hosted-agents/responses/12_foundry_toolbox_mcp_skills/.azdignore b/python/samples/04-hosting/foundry-hosted-agents/responses/12_foundry_toolbox_mcp_skills/.azdignore new file mode 100644 index 00000000000..e228002119e --- /dev/null +++ b/python/samples/04-hosting/foundry-hosted-agents/responses/12_foundry_toolbox_mcp_skills/.azdignore @@ -0,0 +1,6 @@ +agent.manifest.yaml +agent.yaml +.env.example +.env +toolbox.yaml +skills 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 index 31ed562a7e5..67a9a78b35d 100644 --- 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 @@ -5,3 +5,5 @@ __pycache__ *.pyd .Python .env +skills +toolbox.yaml 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 index 0fb7bb652e4..66888c85cbe 100644 --- 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 @@ -1,3 +1,6 @@ FOUNDRY_PROJECT_ENDPOINT="..." AZURE_AI_MODEL_DEPLOYMENT_NAME="..." -TOOLBOX_NAME="..." +# Full versioned MCP endpoint of the skills toolbox the agent consumes. +# Copy this from the output of `azd ai toolbox create ... --from-file ./toolbox.yaml`. +# Example: https://.services.ai.azure.com/api/projects//toolboxes/maf-skills-toolbox/versions/1/mcp?api-version=v1 +TOOLBOX_ENDPOINT="..." 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 index 14ab13f1427..ccd2e46513d 100644 --- 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 @@ -1,80 +1,155 @@ -# What this sample demonstrates +# Agent with Foundry Toolbox MCP Skills (Responses Protocol) -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**. +An [Agent Framework](https://github.com/microsoft/agent-framework) agent that discovers **Agent Skills attached to a Foundry Toolbox** over the **MCP protocol** and exposes them to the model using the [Agent Skills](https://agentskills.io/) progressive-disclosure pattern, hosted on Microsoft Foundry 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: +This sample is **self-contained**: it ships the `SKILL.md` sources and a `toolbox.yaml`, and walks you through creating the skills and the toolbox from zero with `azd` — you don't need an existing toolbox to run it. -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. +## How progressive disclosure works -This way the full skill body and resources are only loaded when the agent actually needs them, reducing token usage. +The `FoundryToolbox` is attached to the agent and its skills are exposed through a `SkillsProvider`. When the agent runs, it discovers the toolbox's skills and applies the progressive-disclosure pattern so a skill's full body is only fetched when the agent actually needs it, reducing token usage: + +1. **Advertise** — each skill's name and description are injected into the system prompt so the model knows what is available (~100 tokens per skill). +2. **Load** — when the model decides a skill is relevant, it retrieves the full `SKILL.md` body on demand via `resources/read`. + +> The Agent Skills spec defines a third stage — **read resources** — where a skill fetches supplementary files (reference documents, assets) on demand. That stage requires skills to be served as `type: skill-md` with sibling resources, but Foundry serves ZIP-uploaded (multi-file) skills as `type: archive`, which toolbox skill discovery does not currently surface. So this sample keeps both skills as single-file `SKILL.md` (advertise + load only). See the [`09_foundry_skills`](../09_foundry_skills/README.md) sample for the same instruction-only pattern via direct download. ## Toolbox MCP skills vs. Foundry Skills Foundry exposes skills in two ways, and this sample uses the second one. -Foundry Skills are managed through the Foundry Skills REST API. An agent downloads each `SKILL.md` as a ZIP at startup, serving the bodies from local files. See the [`09_foundry_skills`](../09_foundry_skills/README.md) sample for a demonstration. +**Foundry Skills** are downloaded directly into an agent: the agent pulls each `SKILL.md` from the Skills API at startup and serves the bodies from local files. See the [`09_foundry_skills`](../09_foundry_skills/README.md) sample. + +**Toolbox MCP skills** are accessed through a toolbox over the MCP protocol. A toolbox bundles a curated set of skills (and optionally tools) behind one MCP endpoint, and any MCP client discovers them automatically. Skill bodies are fetched on demand. The same `SKILL.md` files power both modes — the difference is only in delivery. + +## How it works + +### Model integration -Toolbox MCP skills are accessed through a toolbox over the MCP protocol. A toolbox bundles a curated set of skills behind one MCP endpoint, and any MCP client discovers them automatically. Skill bodies and any supplementary resources are fetched on demand. +[`main.py`](main.py) uses `FoundryChatClient` from the Agent Framework to create an OpenAI-compatible Responses client. It then: -## How It Works +1. Constructs a `FoundryToolbox(credential, load_tools=False)`. The toolbox resolves its MCP endpoint from `TOOLBOX_ENDPOINT`, authenticates every request with the credential, and forwards the platform per-request call-id. `load_tools=False` keeps the toolbox's tools hidden so only its Agent Skills are surfaced. +2. Calls `toolbox.as_skills_provider()`, which discovers skills from the well-known `skill://index.json` resource on the toolbox's MCP session and exposes them as an agent context provider. +3. Passes the toolbox via `tools=` **and** the provider via `context_providers=`. The `tools=` wiring connects the MCP session (the connection the provider reads from); the `context_providers=` wiring runs the advertise/load logic over that session. Both are required — see [main.py](main.py) for the full implementation. -### Model Integration +### Agent hosting -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`. +The agent is hosted with the `ResponsesHostServer`, which provisions a REST API endpoint compatible with the OpenAI Responses protocol on `http://localhost:8088`. -See [main.py](main.py) for the full implementation. +## The bundled skills -### Agent Hosting +This sample ships two source skills under [`skills/`](skills/), reused from the [`09_foundry_skills`](../09_foundry_skills/README.md) sample so you can compare the two delivery modes side by side: -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. +| Skill | Purpose | +|---|---| +| [`support-style`](skills/support-style/SKILL.md) | Voice, formatting, and signature rules for Contoso Outdoors support replies. | +| [`escalation-policy`](skills/escalation-policy/SKILL.md) | When and how to escalate a customer ticket, including the refund-authority matrix. | + +Each file includes a unique `*-CANARY-*` token that the model is asked to echo, so a response proves the model actually **loaded** the skill rather than hallucinating: + +| Artifact | Canary | Proves | +|---|---|---| +| `support-style/SKILL.md` | `STYLE-CANARY-3318` | The model loaded the `support-style` body. | +| `escalation-policy/SKILL.md` | `ESC-CANARY-7742` | The model loaded the `escalation-policy` body. | + +> The `name` and `description` values in the YAML front matter must be **unquoted** — quoting them causes the Skills API to reject the import. ## Prerequisites - Python 3.12+ - A Microsoft Foundry project with a deployed model (e.g., `gpt-5`) -- A Foundry Toolbox with skills attached (see below) -- Azure CLI logged in (`az login`) +- **Azure Developer CLI (`azd`)** 1.25+ with the unified Foundry extension bundle: + ```bash + azd extension install microsoft.foundry + ``` +- Authenticated: `azd auth login` (and `az login` if you run the host with plain `python`) + +### Required RBAC -## Setting up a Foundry Toolbox with skills +Your identity (and, in production, the Managed Identity running the container) needs the **Foundry User** role (formerly *Azure AI User*) on the Foundry project. This covers creating skills, creating the toolbox, and discovering skills over MCP at runtime. -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. +## Building the toolbox from zero -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). +The agent reads the toolbox's MCP endpoint from `TOOLBOX_ENDPOINT`. Before you can run it, create the skills in your Foundry project and then create a toolbox that references them. + +Point `azd` at your project once: + +```bash +azd ai project set "https://.services.ai.azure.com/api/projects/" +``` + +### Step 1 — Create the skills in Foundry + +Skills referenced by a toolbox must already exist in the same Foundry project. Both skills in this sample are single-file `SKILL.md` skills, so upload each directly: + +```bash +azd ai skill create support-style --file ./skills/support-style/SKILL.md --no-prompt +azd ai skill create escalation-policy --file ./skills/escalation-policy/SKILL.md --no-prompt +``` -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`. +> **Why single files (not ZIPs)?** Uploading a skill as a `.zip` (to bundle supplementary resource files) makes Foundry serve it as `type: archive` in the toolbox's `skill://index.json`. Toolbox skill discovery currently surfaces only `type: skill-md` entries, so archive skills are silently dropped. Keeping each skill as a single `SKILL.md` ensures both are discovered. -## Running the Agent Host +> The `name:` in each `SKILL.md` front matter must equal the positional skill name you pass to `azd ai skill create`. To replace a skill after editing it, re-run with `--force` (this deletes the existing skill and all its versions, then uploads a fresh v1). -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. +### Step 2 — Create the toolbox -An extra environment variable must be set to point to the toolbox name: +Create the toolbox once from the bundled [`toolbox.yaml`](toolbox.yaml), which references both skills by name plus one connectionless placeholder tool (`code_interpreter`): ```bash -export TOOLBOX_NAME="my-toolbox" +azd ai toolbox create maf-skills-toolbox --from-file ./toolbox.yaml --no-prompt ``` -Or in PowerShell: +> **Why a placeholder tool?** `azd ai toolbox create` requires at least one `tools` or `connections` entry, so a purely skills-only toolbox cannot be created directly. The bundled `toolbox.yaml` includes a single connectionless `code_interpreter` tool to satisfy this. Because the agent builds the toolbox with `load_tools=False` (see [main.py](main.py)), that tool is never surfaced to the model — only the skills are — so the toolbox stays effectively skills-only from the agent's perspective. + +The first version becomes the default automatically. Use `azd ai toolbox list`, `azd ai toolbox show maf-skills-toolbox`, and `azd ai toolbox version list maf-skills-toolbox` to inspect it, and `azd ai toolbox delete maf-skills-toolbox --force` to remove it. + +### Step 3 — Store the toolbox endpoint -```powershell -$env:TOOLBOX_NAME="my-toolbox" +`azd ai toolbox create` prints the toolbox's **versioned** MCP endpoint. Copy it and store it so the agent connects to it: + +```bash +azd env set TOOLBOX_ENDPOINT "https://.services.ai.azure.com/api/projects//toolboxes/maf-skills-toolbox/versions/1/mcp?api-version=v1" ``` -You can also place these in a `.env` file next to `main.py` — see [`.env.example`](.env.example). +When running the host with plain `python`, put the same value in a `.env` file next to `main.py` instead — see [`.env.example`](.env.example). + +## Running the agent host + +Follow the [Running the Agent Host Locally](../../README.md#running-the-agent-host-locally) section of the parent README to run the host with either `azd ai agent run` or plain `python main.py`. This sample requires `TOOLBOX_ENDPOINT` to be set (see Step 3) in addition to the standard `FOUNDRY_PROJECT_ENDPOINT` and `AZURE_AI_MODEL_DEPLOYMENT_NAME` variables. ## 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. +> Depending on how you run the agent host, you can invoke the agent using `curl` (`Invoke-WebRequest` in PowerShell) or `azd`. See the [parent README](../../README.md) for details. Use this README for sample queries. -Send a POST request to the server with a JSON body containing an `"input"` field to interact with the agent. For example: +Send a POST request with a JSON body containing an `"input"` field: ```bash +# Discover what the toolbox advertises (advertise step only) curl -X POST http://localhost:8088/responses -H "Content-Type: application/json" -d '{"input": "What skills do you have available?"}' + +# Routine question -> loads support-style +curl -X POST http://localhost:8088/responses -H "Content-Type: application/json" -d '{"input": "Hi, I am Alex. Can I return my tent within 30 days?"}' + +# Large refund + legal threat -> loads escalation-policy (which includes the refund matrix) +curl -X POST http://localhost:8088/responses -H "Content-Type: application/json" -d '{"input": "I want a $750 refund on Order #A-1042 right now or I am calling my lawyer."}' +``` + +| Prompt mentions | Skill that should drive the response | Canary you should see | +|---|---|---| +| Routine return / shipping / care question | `support-style` | `STYLE-CANARY-3318` | +| Injury, legal threat, press, or refund > $500 | `escalation-policy` (+ `support-style`) | `ESC-CANARY-7742` | + +Because skills are loaded on demand, a canary token in a response proves the model actually invoked `load_skill` for the matching skill — not that it merely saw the name in the advertised list. + +## Deploying the agent to Foundry + +Once tested locally, deploy to Microsoft Foundry by following the [Deploying the Agent to Foundry](../../README.md#deploying-the-agent-to-foundry) section of the parent README. + +Make sure the skills and toolbox exist in the **same** Foundry project you deploy to (run the steps above against it first), and that `TOOLBOX_ENDPOINT` is set in your `azd` environment so it is injected into the hosted container per [`agent.manifest.yaml`](agent.manifest.yaml): + +```bash +azd env set TOOLBOX_ENDPOINT "" ``` -## Deploying the Agent to Foundry +The deployed agent's Managed Identity needs the **Foundry User** role on the Foundry project to discover skills over MCP at startup. -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. +> The bundled `skills/` folder and `toolbox.yaml` are authoring inputs only; they are excluded from the deployed container via [`.azdignore`](.azdignore) / [`.dockerignore`](.dockerignore). The running agent discovers everything it needs from the toolbox MCP endpoint. 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 index e2fd0a44c29..148e7ad11fa 100644 --- 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 @@ -29,13 +29,14 @@ template: 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 + - name: TOOLBOX_ENDPOINT + # Full versioned MCP endpoint of the skills toolbox the agent consumes. + # Create the skills and toolbox from the bundled skills/ folder and + # toolbox.yaml, then copy the versioned endpoint it prints and store it + # in your azd environment before you run or deploy: + # azd ai toolbox create maf-skills-toolbox --from-file ./toolbox.yaml + # azd env set TOOLBOX_ENDPOINT "" + value: "{{TOOLBOX_ENDPOINT}}" resources: - kind: model id: gpt-5 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 index e73b796b363..75d676ed187 100644 --- 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 @@ -10,5 +10,11 @@ resources: environment_variables: - name: AZURE_AI_MODEL_DEPLOYMENT_NAME value: ${AZURE_AI_MODEL_DEPLOYMENT_NAME} - - name: TOOLBOX_NAME - value: ${TOOLBOX_NAME} + - name: TOOLBOX_ENDPOINT + # Full versioned MCP endpoint of the skills toolbox the agent consumes. + # Create the skills and toolbox from the bundled skills/ folder and + # toolbox.yaml, then copy the versioned endpoint it prints and store it in + # your azd environment before you run or deploy: + # azd ai toolbox create maf-skills-toolbox --from-file ./toolbox.yaml + # azd env set TOOLBOX_ENDPOINT "" + value: ${TOOLBOX_ENDPOINT} 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 index 8165dc2ef9b..6ce2fb3561e 100644 --- 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 @@ -26,8 +26,11 @@ async def main() -> None: # as_skills_provider() discovers skills from skill://index.json on the toolbox # MCP session and exposes them as an agent context provider; SKILL.md bodies are - # fetched on demand via resources/read. - skills_provider = toolbox.as_skills_provider() + # fetched on demand via resources/read. disable_load_skill_approval=True registers + # the load_skill tool with approval_mode="never_require" so this unattended agent + # can load skills without an approval round-trip -- the Responses host runs the + # agent without an AgentSession, which the default approval flow requires. + skills_provider = toolbox.as_skills_provider(disable_load_skill_approval=True) client = FoundryChatClient( project_endpoint=os.environ["FOUNDRY_PROJECT_ENDPOINT"], diff --git a/python/samples/04-hosting/foundry-hosted-agents/responses/12_foundry_toolbox_mcp_skills/skills/escalation-policy/SKILL.md b/python/samples/04-hosting/foundry-hosted-agents/responses/12_foundry_toolbox_mcp_skills/skills/escalation-policy/SKILL.md new file mode 100644 index 00000000000..c9ca3b57b72 --- /dev/null +++ b/python/samples/04-hosting/foundry-hosted-agents/responses/12_foundry_toolbox_mcp_skills/skills/escalation-policy/SKILL.md @@ -0,0 +1,47 @@ +--- +name: escalation-policy +description: When and how to escalate Contoso Outdoors customer-support tickets. +--- + +# Contoso Outdoors Escalation Policy + +You must follow this escalation policy on every conversation. + +## Escalate immediately when the customer + +- Reports an injury, allergic reaction, or other safety incident. +- Mentions legal action, regulators, or the press. +- Has waited more than 14 days for a refund that was already approved. +- Requests a refund larger than $500. + +## How to escalate + +1. Acknowledge the issue in one sentence. +2. Tell the customer you are escalating to a senior specialist. +3. Provide the escalation reference `ESC-CANARY-7742` and the SLA: a senior + specialist will reply within 1 business day. +4. Do not promise a specific outcome (refund, replacement, compensation) on + escalated tickets — only the senior specialist can commit to one. + +## Refund authority + +Before quoting any refund amount or approval timeline, apply this matrix and +route the request to the correct approver. Quote the tier and approver exactly. + +| Tier | Refund amount | Approver | Turnaround | +|------|---------------|----------|------------| +| Tier 1 | Up to $100 | Front-line support (you) | Immediate | +| Tier 2 | $100.01 – $500 | Support team lead | Same business day | +| Tier 3 | Over $500 | Senior specialist (escalation required) | 1 business day | + +- A Tier 3 refund always requires escalation per the rules above; you may not + approve it yourself. +- Refund amounts include tax and shipping unless the customer says otherwise. +- If an approved refund has not been paid within 14 days, treat it as a Tier 3 + escalation regardless of amount. + +## Do not escalate + +- Routine returns within the standard 30-day window. +- Shipping status questions. +- Product care and usage questions. diff --git a/python/samples/04-hosting/foundry-hosted-agents/responses/12_foundry_toolbox_mcp_skills/skills/support-style/SKILL.md b/python/samples/04-hosting/foundry-hosted-agents/responses/12_foundry_toolbox_mcp_skills/skills/support-style/SKILL.md new file mode 100644 index 00000000000..a516811a488 --- /dev/null +++ b/python/samples/04-hosting/foundry-hosted-agents/responses/12_foundry_toolbox_mcp_skills/skills/support-style/SKILL.md @@ -0,0 +1,25 @@ +--- +name: support-style +description: Contoso Outdoors customer-support tone and formatting guidelines. +--- + +# Contoso Outdoors Support Style + +You are speaking on behalf of Contoso Outdoors customer support. + +## Voice + +- Warm, concise, and confident — never apologetic in a hand-wringing way. +- Use the customer's name when it is known. +- Sign every response with `— Contoso Outdoors Support`. + +## Formatting + +- Keep replies to 1–3 short paragraphs unless the customer asks for detail. +- Use bullet lists only when enumerating concrete steps or options. +- Always reference order numbers as `Order #` (e.g. `Order #A-1042`). + +## Canary + +To prove this skill was loaded, include the token `STYLE-CANARY-3318` on a +separate line at the bottom of every response, prefixed with `# `. diff --git a/python/samples/04-hosting/foundry-hosted-agents/responses/12_foundry_toolbox_mcp_skills/toolbox.yaml b/python/samples/04-hosting/foundry-hosted-agents/responses/12_foundry_toolbox_mcp_skills/toolbox.yaml new file mode 100644 index 00000000000..f0fcbfe2eb8 --- /dev/null +++ b/python/samples/04-hosting/foundry-hosted-agents/responses/12_foundry_toolbox_mcp_skills/toolbox.yaml @@ -0,0 +1,34 @@ +# toolbox.yaml +# Defines the toolbox this agent consumes. Its purpose is to serve the two +# Agent Skills below over MCP. +# +# NOTE: `azd ai toolbox create` requires at least one `tools` or `connections` +# entry — a skills-only toolbox cannot be created directly. We therefore include +# one connectionless built-in tool (`code_interpreter`) purely to satisfy that +# requirement. The agent constructs the toolbox with `load_tools=False` +# (see main.py), so this tool is never surfaced to the model — only the skills +# are. The toolbox is effectively skills-only from the agent's perspective. +# +# The skills referenced below must already exist in the same Foundry project — +# create them first from the bundled skills/ folder: +# +# azd ai skill create support-style --file ./skills/support-style/SKILL.md --no-prompt +# azd ai skill create escalation-policy --file ./skills/escalation-policy/SKILL.md --no-prompt +# +# Then create the toolbox once from this file: +# +# azd ai toolbox create maf-skills-toolbox --from-file ./toolbox.yaml --no-prompt +# +# The command prints the versioned MCP endpoint. Copy it and set it as the +# TOOLBOX_ENDPOINT environment variable. The agent connects to that endpoint at +# runtime and discovers these skills over MCP. +description: Contoso Outdoors support skills served over MCP +# Placeholder connectionless tool — required by the CLI, hidden by load_tools=False. +tools: + - type: code_interpreter + container: { type: auto } + name: code_interpreter +# Agent Skills discovered over MCP via skill://index.json. +skills: + - name: support-style # follows the skill's default version + - name: escalation-policy # follows the skill's default version