Resolve PR comments

This commit is contained in:
Peter Ibekwe
2026-05-21 21:56:27 -07:00
Unverified
parent 111648ee5b
commit 894b2c6ce0
3 changed files with 79 additions and 303 deletions
@@ -1,94 +1,27 @@
# Copyright (c) Microsoft. All rights reserved.
"""Invoke Foundry Toolbox MCP sample — combines an MCP server tool and a
Foundry built-in tool through a single Foundry **toolbox** endpoint.
"""Invoke a Foundry toolbox MCP endpoint from a declarative workflow.
A Foundry toolbox bundles multiple tool definitions (MCP servers, built-in
Foundry tools such as ``web_search``, etc.) behind a single MCP-compatible
proxy URL. Calling MCP-server-backed tools through the toolbox returns
results namespaced as ``<server_label>___<tool_name>``; calling built-in
tools (e.g. ``web_search``) returns the tool under its plain name.
The workflow lists the toolbox's tools, queries Microsoft Learn Docs
and ``web_search`` through the toolbox, and summarises the combined
results with a Foundry agent. The reserved ``tools/list`` tool name is
intercepted natively by ``DefaultMCPToolHandler``.
This sample mirrors the .NET sample
``dotnet/samples/03-workflows/Declarative/InvokeFoundryToolboxMcp/`` and
focuses on the **workflow execution path**:
Required env vars:
FOUNDRY_PROJECT_ENDPOINT, FOUNDRY_MODEL.
1. Build a bearer-authenticated ``httpx.AsyncClient`` for the toolbox
MCP proxy and hand it to :class:`DefaultMCPToolHandler` so the YAML
can call MCP tools (and introspect the toolbox tool list via the
reserved ``"tools/list"`` tool name handled natively by the
framework, matching .NET
``DefaultMcpToolHandler.ListToolsToolName``).
2. Configure a :class:`WorkflowFactory` with that handler plus a local
:class:`Agent` registered by name so the YAML's ``InvokeAzureAgent``
action can summarise the combined tool output.
3. Drive the workflow with a user question and render per-action
progress markers plus the final agent summary.
One-off **toolbox administration** (delete + create_version) is delegated
to :mod:`toolbox_provisioning` so this file stays focused on the workflow.
Security note:
The default ``DefaultMCPToolHandler`` performs no URL allowlisting or
SSRF protection. This sample uses a project-scoped ``client_provider``
that pins outbound requests to ``Authorization: Bearer …`` via Azure
AD AND fails closed (raises) when the YAML resolves a different
``serverUrl``, so a tampered ``=Env.*`` value cannot redirect the
bearer token to an attacker-controlled URL. MCP outputs flow back
into agent conversations and share the prompt-injection risk
surface of any other tool output.
Optional env vars:
FOUNDRY_TOOLBOX_NAME, FOUNDRY_TOOLBOX_API_VERSION,
FOUNDRY_TOOLBOX_DOCS_SERVER_LABEL,
FOUNDRY_TOOLBOX_WEB_SEARCH_TOOL_NAME, FOUNDRY_TOOLBOX_ENDPOINT.
Run with:
python samples/03-workflows/declarative/invoke_foundry_toolbox_mcp/main.py
Required environment variables:
FOUNDRY_PROJECT_ENDPOINT
Azure AI Foundry project endpoint.
FOUNDRY_MODEL
Deployed Foundry model name used by ``FoundryChatClient``.
Optional environment variables:
FOUNDRY_TOOLBOX_NAME
Name of the toolbox to (re)create. Defaults to
``declarative_foundry_toolbox_mcp``.
FOUNDRY_TOOLBOX_API_VERSION
Toolbox MCP API version used when building the endpoint URL.
Defaults to ``v1``.
FOUNDRY_TOOLBOX_DOCS_SERVER_LABEL
The ``server_label`` registered for the Microsoft Learn Docs MCP
server in the toolbox. Tool names from that server get the
``<server_label>___`` prefix on the toolbox MCP proxy.
Defaults to ``microsoft_docs``.
FOUNDRY_TOOLBOX_WEB_SEARCH_TOOL_NAME
Name of the Foundry built-in web-search tool surfaced by the
toolbox. Defaults to ``web_search``.
FOUNDRY_TOOLBOX_ENDPOINT
Explicit toolbox MCP endpoint URL. When set, overrides the URL
computed from ``FOUNDRY_PROJECT_ENDPOINT``,
``FOUNDRY_TOOLBOX_NAME``, and ``FOUNDRY_TOOLBOX_API_VERSION``.
Sample output:
============================================================
Invoke Foundry Toolbox MCP Workflow Demo
============================================================
Provisioning toolbox 'declarative_foundry_toolbox_mcp' in Foundry...
Toolbox endpoint: https://<account>.services.ai.azure.com/api/projects/<project>/toolboxes/declarative_foundry_toolbox_mcp/mcp?api-version=v1
Ask one question that benefits from both Microsoft Learn docs and a web search.
You: How do I configure logging in the Agent Framework?
[Listing toolbox tools...]
[Searching Microsoft Learn docs...]
[Searching the web...]
[Summarizing results...]
Agent: The Agent Framework declarative workflow runtime ...
"""
import asyncio
import os
from collections.abc import Iterator
from collections.abc import Generator
from pathlib import Path
import httpx
@@ -102,48 +35,13 @@ from agent_framework.foundry import FoundryChatClient
from azure.core.credentials import TokenCredential
from azure.identity import AzureCliCredential, get_bearer_token_provider
from toolbox_provisioning import (
AZ_CLI_PROCESS_TIMEOUT_SECONDS,
FOUNDRY_FEATURES_HEADERS,
build_toolbox_mcp_server_url,
create_sample_toolbox,
)
DEFAULT_TOOLBOX_NAME = "declarative_foundry_toolbox_mcp"
DEFAULT_TOOLBOX_API_VERSION = "v1"
DEFAULT_DOCS_SERVER_LABEL = "microsoft_docs"
DEFAULT_WEB_SEARCH_TOOL_NAME = "web_search"
AGENT_NAME = "FoundryToolboxMcpAgent"
# YAML action ids — kept in sync with ``workflow.yaml`` so the host can
# render progress markers as each step starts. Long-running MCP calls
# and a slow Foundry agent invocation can otherwise look like a hang.
LIST_TOOLS_ACTION_ID = "list_toolbox_tools"
DOCS_SEARCH_ACTION_ID = "search_docs_with_toolbox"
WEB_SEARCH_ACTION_ID = "search_web_with_toolbox"
SUMMARIZE_ACTION_ID = "summarize_toolbox_result"
_ACTION_PROGRESS_LABELS: dict[str, str] = {
LIST_TOOLS_ACTION_ID: "Listing toolbox tools...",
DOCS_SEARCH_ACTION_ID: "Searching Microsoft Learn docs...",
WEB_SEARCH_ACTION_ID: "Searching the web...",
SUMMARIZE_ACTION_ID: "Summarizing results...",
}
# AAD audience for the toolbox MCP proxy. Same scope used by the existing
# Foundry hosted-toolbox samples.
TOOLBOX_AAD_SCOPE = "https://ai.azure.com/.default"
# Match the MCP-recommended httpx timeouts (``mcp.shared._httpx_utils``:
# 30s connect/write/pool, 5min SSE read). httpx's default ``Timeout(5.0)``
# is far too aggressive for MCP streaming responses — long-running
# tool calls through the Foundry toolbox MCP proxy (e.g. the built-in
# ``web_search``) can take longer than 5s, and a read-timeout fired
# mid-stream leaves the upper-level ``call_tool`` awaiting a future that
# never resolves, surfacing as an indefinite hang.
MCP_CONNECT_TIMEOUT_SECONDS = 30.0
MCP_READ_TIMEOUT_SECONDS = 300.0
AGENT_INSTRUCTIONS = """\
You combine results from two tool calls in the conversation:
@@ -159,37 +57,28 @@ result set contains an answer, say so plainly rather than guessing.
class _BearerAuth(httpx.Auth):
"""Inject a fresh Azure AD bearer token on every request.
``httpx.Auth.auth_flow`` is a sync generator and works for both sync
and async clients. ``get_bearer_token_provider`` caches/refreshes the
token internally, so calling it per request is cheap.
"""
"""Inject a fresh Azure AD bearer token on every request."""
def __init__(self, credential: TokenCredential) -> None:
self._get_token = get_bearer_token_provider(credential, TOOLBOX_AAD_SCOPE)
self._get_token = get_bearer_token_provider(credential, "https://ai.azure.com/.default")
def auth_flow(self, request: httpx.Request) -> Iterator[httpx.Request]:
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:
"""Run the Foundry toolbox MCP workflow."""
# 1. Read configuration. ``FOUNDRY_PROJECT_ENDPOINT`` and
# ``FOUNDRY_MODEL`` are required; everything else has defaults.
project_endpoint = os.environ["FOUNDRY_PROJECT_ENDPOINT"]
model = os.environ["FOUNDRY_MODEL"]
toolbox_name = os.environ.get("FOUNDRY_TOOLBOX_NAME", DEFAULT_TOOLBOX_NAME)
toolbox_api_version = os.environ.get("FOUNDRY_TOOLBOX_API_VERSION", DEFAULT_TOOLBOX_API_VERSION)
docs_server_label = os.environ.get("FOUNDRY_TOOLBOX_DOCS_SERVER_LABEL", DEFAULT_DOCS_SERVER_LABEL)
web_search_tool_name = os.environ.get("FOUNDRY_TOOLBOX_WEB_SEARCH_TOOL_NAME", DEFAULT_WEB_SEARCH_TOOL_NAME)
toolbox_name = os.environ.get("FOUNDRY_TOOLBOX_NAME", "declarative_foundry_toolbox_mcp")
toolbox_api_version = os.environ.get("FOUNDRY_TOOLBOX_API_VERSION", "v1")
docs_server_label = os.environ.get("FOUNDRY_TOOLBOX_DOCS_SERVER_LABEL", "microsoft_docs")
web_search_tool_name = os.environ.get("FOUNDRY_TOOLBOX_WEB_SEARCH_TOOL_NAME", "web_search")
print("=" * 60)
print("Invoke Foundry Toolbox MCP Workflow Demo")
print("=" * 60)
# 2. Provision the toolbox in Foundry. Idempotent: delete-then-create.
print(f"Provisioning toolbox '{toolbox_name}' in Foundry...")
create_sample_toolbox(
name=toolbox_name,
@@ -197,16 +86,14 @@ async def main() -> None:
project_endpoint=project_endpoint,
)
# 3. Resolve the toolbox MCP proxy URL. The workflow YAML references
# these values via ``=Env.FOUNDRY_TOOLBOX_*``; we publish them
# through ``WorkflowFactory(configuration=...)`` so the values stay scoped to
# this workflow.
toolbox_endpoint = os.environ.get("FOUNDRY_TOOLBOX_ENDPOINT") or build_toolbox_mcp_server_url(
project_endpoint=project_endpoint,
name=toolbox_name,
api_version=toolbox_api_version,
)
workflow_configuration: dict[str, str] = {
# Values exposed to ``=Env.*`` in workflow.yaml. Passing them via
# ``configuration`` keeps the symbol table scoped to this workflow.
workflow_configuration = {
"FOUNDRY_TOOLBOX_MCP_SERVER_URL": toolbox_endpoint,
"FOUNDRY_TOOLBOX_DOCS_SERVER_LABEL": docs_server_label,
"FOUNDRY_TOOLBOX_WEB_SEARCH_TOOL_NAME": web_search_tool_name,
@@ -214,64 +101,27 @@ async def main() -> None:
print(f"Toolbox endpoint: {toolbox_endpoint}")
print()
# 4. Build the Foundry chat client + the summarising agent. The agent
# is registered with the factory by name, matching the sibling
# ``invoke_mcp_tool/`` sample.
credential = AzureCliCredential(process_timeout=AZ_CLI_PROCESS_TIMEOUT_SECONDS)
chat_client = FoundryChatClient(
project_endpoint=project_endpoint,
model=model,
credential=credential,
)
summary_agent = Agent(
client=chat_client,
name=AGENT_NAME,
instructions=AGENT_INSTRUCTIONS,
)
credential = AzureCliCredential()
chat_client = FoundryChatClient(project_endpoint=project_endpoint, model=model, credential=credential)
summary_agent = Agent(client=chat_client, name=AGENT_NAME, instructions=AGENT_INSTRUCTIONS)
# 5. Build a bearer-authenticated httpx client. The same client is
# reused for every MCP request: the LRU cache inside
# ``DefaultMCPToolHandler`` keeps a single MCP session alive
# for the toolbox URL, and ``tools/list`` reuses that same
# cached session for full transport-level consistency.
#
# Key configuration choices:
# * ``headers=FOUNDRY_FEATURES_HEADERS`` attaches the
# ``Foundry-Features: Toolboxes=V1Preview`` flag to EVERY
# outbound request — including the MCP ``initialize`` handshake
# during ``connect()``. The YAML's per-action ``headers:`` block
# also sets this value but only takes effect during
# ``call_tool`` (the ``MCPStreamableHTTPTool`` header_provider
# contextvar is empty during connect — see
# ``python/packages/core/agent_framework/_mcp.py:1639-1645``).
# Without the client-level default the toolbox MCP proxy rejects
# the session handshake and surfaces "unhandled errors in a
# TaskGroup".
# * ``timeout=Timeout(30.0, read=300.0)`` matches the MCP
# recommended defaults (``mcp.shared._httpx_utils``: 30s
# connect/write/pool, 5min SSE read). The httpx defaults of 5s
# EVERYWHERE break long-running MCP tool calls — the Foundry
# built-in ``web_search``, for instance, can take longer than
# 5s to return through the toolbox SSE stream and would
# otherwise leave the client waiting on a future that never
# resolves (i.e. visibly hang on the host).
# * ``follow_redirects=True`` also mirrors the MCP defaults so
# proxy redirects don't surface as broken streams.
# ``headers=`` attaches the Foundry-Features preview flag on every
# request, including the MCP ``initialize`` handshake (the YAML's
# per-action ``headers`` only takes effect during ``call_tool``).
# ``timeout=`` matches the MCP-recommended values; httpx's 5s
# default breaks long-running tool calls like ``web_search``.
http_client = httpx.AsyncClient(
auth=_BearerAuth(credential),
headers=FOUNDRY_FEATURES_HEADERS,
timeout=httpx.Timeout(MCP_CONNECT_TIMEOUT_SECONDS, read=MCP_READ_TIMEOUT_SECONDS),
timeout=httpx.Timeout(30.0, read=300.0),
follow_redirects=True,
)
async def _client_provider(invocation: MCPToolInvocation) -> httpx.AsyncClient | None:
# Pin the bearer-authenticated client to the resolved toolbox URL.
# The Foundry AAD bearer token is scoped to ``https://ai.azure.com``
# but we still refuse to attach it to any URL we did not provision —
# if the YAML resolves a different ``serverUrl`` (e.g. via a tampered
# ``Env.*`` value or a config injection), fail closed by raising so
# ``DefaultMCPToolHandler`` cannot fall back to an unauthenticated
# client that silently leaks the request shape.
# Fail closed when the YAML resolves a different ``serverUrl``
# so the bearer-bound client cannot be reused against an
# unexpected endpoint and ``DefaultMCPToolHandler`` cannot
# silently fall back to an unauthenticated client.
if invocation.server_url.casefold() != toolbox_endpoint.casefold():
raise ValueError(
f"Refusing to attach Foundry bearer token to unexpected MCP URL: "
@@ -286,55 +136,35 @@ async def main() -> None:
factory = WorkflowFactory(
agents={AGENT_NAME: summary_agent},
mcp_tool_handler=mcp_handler,
# The workflow YAML references ``=Env.FOUNDRY_TOOLBOX_*`` to keep
# the toolbox URL / tool names configurable without editing the
# YAML. We supply those values through ``configuration`` so the
# PowerFx ``Env`` symbol is populated from a local dict instead
# of the process environment. ``restrict_env_to_configuration``
# defaults to ``True`` which suppresses any ``os.environ``
# fallback — the workflow only sees the keys explicitly listed
# in ``workflow_configuration`` below.
configuration=workflow_configuration,
)
workflow_path = Path(__file__).parent / "workflow.yaml"
workflow = factory.create_workflow_from_yaml_path(workflow_path)
workflow = factory.create_workflow_from_yaml_path(Path(__file__).parent / "workflow.yaml")
print("Ask one question that benefits from both Microsoft Learn docs and a web search.")
print()
user_input = input("You: ").strip() # noqa: ASYNC250
if not user_input:
user_input = "How do I configure logging in the Agent Framework?"
user_input = input("You: ").strip() or "How do I configure logging in the Agent Framework?" # noqa: ASYNC250
# 6. Drive the workflow with the user's question. The YAML fans
# out three MCP calls and finishes with the InvokeAzureAgent
# summarisation step. We render two kinds of host-visible
# feedback:
#
# * Per-action progress lines via ``executor_invoked``
# events so a slow MCP call or agent invocation cannot
# look like a hang.
# * The final agent summary via ``output`` events. The
# three MCP actions use ``autoSend: false`` in the YAML
# so only the summarising agent's text reaches this
# branch.
# Progress markers per YAML action so slow MCP calls or agent
# invocations don't look like a hang. Action ids mirror
# workflow.yaml.
progress_labels = {
"list_toolbox_tools": "Listing toolbox tools...",
"search_docs_with_toolbox": "Searching Microsoft Learn docs...",
"search_web_with_toolbox": "Searching the web...",
"summarize_toolbox_result": "Summarizing results...",
}
printed_prefix = False
produced_output = False
agent_executor_id = SUMMARIZE_ACTION_ID
async for event in workflow.run({"text": user_input}, stream=True):
if event.type == "executor_invoked":
label = _ACTION_PROGRESS_LABELS.get(event.executor_id or "")
label = progress_labels.get(event.executor_id or "")
if label is not None:
print(f"[{label}]")
continue
if event.type == "output" and isinstance(event.data, str):
# Only the summarising agent action sends an output
# event (MCP calls use ``autoSend: false``). Guard the
# display so any future autoSend additions still print
# under the "Agent:" prefix only when they come from
# that action.
if event.executor_id and event.executor_id != agent_executor_id:
# Only the summarising agent emits ``output``; the three
# MCP actions use ``autoSend: false`` in the YAML.
if event.executor_id and event.executor_id != "summarize_toolbox_result":
continue
if not printed_prefix:
print("\nAgent: ", end="", flush=True)
@@ -1,67 +1,31 @@
# Copyright (c) Microsoft. All rights reserved.
"""Foundry toolbox provisioning helper for the ``invoke_foundry_toolbox_mcp`` sample.
"""Foundry toolbox provisioning helper for ``invoke_foundry_toolbox_mcp``.
This module is intentionally narrow: it covers the one-off **administrative**
setup needed to (re)create a Foundry toolbox so the sample can be run
end-to-end without manual portal/CLI steps. Workflow execution, MCP
handling, and agent orchestration live in :mod:`main`.
Toolboxes are normally provisioned through the Foundry portal or a separate
deployment script. Bundling the provisioning step here keeps the sample
self-contained and re-runnable.
The Foundry-Features preview header is exported here as well so the
runtime MCP client in ``main.py`` can attach it on every outbound request
(the MCP ``initialize`` handshake also requires the flag, not just the
toolbox administration calls).
Toolboxes are normally provisioned through the Foundry portal or a
separate deployment script; bundling the setup here lets the sample run
end-to-end without manual steps. ``main.py`` owns the workflow execution
path.
"""
from collections.abc import Mapping
from azure.identity import AzureCliCredential
DEFAULT_DOCS_MCP_SERVER_URL = "https://learn.microsoft.com/api/mcp"
# Bump the ``az.cmd`` subprocess timeout from the default 10s. On Windows
# the Azure CLI batch wrapper can take noticeably longer than 10s to
# return a token (cold-start + ``az`` self-update checks + AAD round-trip),
# which surfaces as ``CredentialUnavailableError: Failed to invoke the
# Azure CLI`` after a ``subprocess.TimeoutExpired`` from the credential's
# internal call.
AZ_CLI_PROCESS_TIMEOUT_SECONDS = 60
# Toolbox administration AND runtime MCP traffic are both gated by an
# Azure AI Foundry preview feature flag. The .NET sample injects this
# header via a pipeline policy on the ``AgentAdministrationClient``;
# the Python ``AIProjectClient`` doesn't add it automatically, so we pass
# it as a per-call header on every toolbox admin operation (delete +
# create_version) here, and the runtime code in ``main.py`` attaches it
# as a default header on the ``httpx.AsyncClient`` so it travels on the
# MCP ``initialize`` handshake as well. Without this header on admin
# calls, provisioning succeeds at the HTTP layer but the toolbox is
# never wired up to the MCP endpoint — surfacing at runtime as "MCP
# server failed to initialize: Session terminated" on the first
# ``InvokeMcpTool`` call.
FOUNDRY_FEATURES_HEADER_NAME = "Foundry-Features"
FOUNDRY_FEATURES_HEADER_VALUE = "Toolboxes=V1Preview"
FOUNDRY_FEATURES_HEADERS: Mapping[str, str] = {
FOUNDRY_FEATURES_HEADER_NAME: FOUNDRY_FEATURES_HEADER_VALUE,
}
# Toolbox admin and MCP runtime traffic are both gated by a preview
# feature flag. The Python ``AIProjectClient`` does not add it
# automatically, so we attach it to every admin call here AND to the
# ``httpx.AsyncClient`` in ``main.py`` so the MCP ``initialize``
# handshake carries it too. Without the flag on admin calls,
# provisioning succeeds at the HTTP layer but the toolbox is never
# wired to the MCP endpoint — surfacing later as "MCP server failed to
# initialize: Session terminated" on the first ``InvokeMcpTool`` call.
FOUNDRY_FEATURES_HEADERS: Mapping[str, str] = {"Foundry-Features": "Toolboxes=V1Preview"}
def build_toolbox_mcp_server_url(project_endpoint: str, name: str, api_version: str) -> str:
"""Compose the Foundry toolbox MCP proxy URL.
Toolboxes provisioned via ``AIProjectClient.beta.toolboxes`` live under
the ``/toolboxes/{name}`` resource path (the Python SDK's
``BetaToolboxesOperations`` routes POST/GET/DELETE there — see
``azure/ai/projects/operations/_operations.py``). Their MCP proxy URL
is ``<project_endpoint>/toolboxes/{name}/mcp?api-version=<api_version>``,
matching the .NET sample.
"""
base = project_endpoint.rstrip("/")
return f"{base}/toolboxes/{name}/mcp?api-version={api_version}"
"""Compose the Foundry toolbox MCP proxy URL."""
return f"{project_endpoint.rstrip('/')}/toolboxes/{name}/mcp?api-version={api_version}"
def create_sample_toolbox(
@@ -69,27 +33,21 @@ def create_sample_toolbox(
name: str,
docs_server_label: str,
project_endpoint: str,
docs_server_url: str = DEFAULT_DOCS_MCP_SERVER_URL,
docs_server_url: str = "https://learn.microsoft.com/api/mcp",
) -> None:
"""Provision a toolbox version in the Foundry project (idempotent).
"""Provision a toolbox version (delete-then-create; idempotent).
Deletes any existing toolbox under ``name`` and then creates a new
version that bundles:
- the Microsoft Learn Docs MCP server
(``server_label=docs_server_label``), and
- the Foundry built-in ``web_search`` tool.
Uses ``AzureCliCredential`` because the sample is meant to be run by
a developer with ``az login`` already configured; switch to a managed
identity / service principal credential for production deployments.
Bundles the Microsoft Learn Docs MCP server and the Foundry built-in
``web_search`` tool. Uses ``AzureCliCredential`` because the sample
expects ``az login``; switch to a managed identity or service
principal for production deployments.
"""
from azure.ai.projects import AIProjectClient
from azure.ai.projects.models import MCPTool, Tool, WebSearchTool
from azure.core.exceptions import ResourceNotFoundError
with (
AzureCliCredential(process_timeout=AZ_CLI_PROCESS_TIMEOUT_SECONDS) as credential,
AzureCliCredential() as credential,
AIProjectClient(credential=credential, endpoint=project_endpoint) as project_client,
):
try:
@@ -99,11 +57,7 @@ def create_sample_toolbox(
pass
tools: list[Tool] = [
MCPTool(
server_label=docs_server_label,
server_url=docs_server_url,
require_approval="never",
),
MCPTool(server_label=docs_server_label, server_url=docs_server_url, require_approval="never"),
WebSearchTool(),
]
@@ -40,19 +40,11 @@ trigger:
variable: Local.SearchQuery
value: =Workflow.Inputs.text
# List the tools exposed by the toolbox MCP proxy.
#
# We intentionally OMIT ``conversationId`` here: the tool list is
# metadata for the demo, not useful context for the downstream
# summarising agent. Forwarding it into the conversation only
# inflates token usage and per-call latency. We also keep
# ``autoSend: false`` because the Python host's streaming loop
# prints every string ``output`` event — emitting the raw tool
# list JSON would visually bury the agent's final answer. The
# .NET sibling sample uses ``autoSend: true`` because the .NET
# ``WorkflowRunner`` console helper only renders agent updates
# (not ``WorkflowOutputEvent``s), so the same YAML value behaves
# differently across hosts.
# List the tools exposed by the toolbox MCP proxy. We omit
# ``conversationId`` (the catalog is demo metadata, not useful
# context for the downstream agent) and keep ``autoSend: false``
# so the raw JSON catalog doesn't bury the agent's final answer in
# the host's output stream.
- kind: InvokeMcpTool
id: list_toolbox_tools
serverUrl: =Env.FOUNDRY_TOOLBOX_MCP_SERVER_URL