Revamped sample to address PR comments.

This commit is contained in:
Peter Ibekwe
2026-05-22 08:58:19 -07:00
Unverified
parent 894b2c6ce0
commit 6ea7b45290
8 changed files with 118 additions and 233 deletions
@@ -63,24 +63,19 @@ logger = logging.getLogger(__name__)
_ENV_REFERENCE_RE = re.compile(r"\bEnv\.([A-Za-z_][A-Za-z0-9_]*)")
_EMPTY_ENV_VALUES: Mapping[str, str] = MappingProxyType({})
_EMPTY_ENV_REFERENCES: frozenset[str] = frozenset()
@dataclass(frozen=True)
class DeclarativeEnvConfig:
"""Configuration that populates the PowerFx ``Env`` symbol for a workflow.
Mirrors the .NET ``IConfiguration``-driven environment binding in
``Microsoft.Agents.AI.Workflows.Declarative/PowerFx/WorkflowDiagnostics.cs``.
The configuration values are always exposed under ``Env.<name>`` (matching
``IConfiguration`` semantics), and ``os.environ`` is consulted only when
``restrict_to_configuration`` is ``False`` and the YAML literally
references the name in a PowerFx expression.
Configuration values are always exposed under ``Env.<name>``;
``os.environ`` is consulted only when ``restrict_to_configuration``
is ``False`` AND the YAML literally references the name in a PowerFx
expression (the allowlist enforced via ``referenced_names``).
Attributes:
values: Caller-supplied configuration values resolved by name when
the workflow YAML references ``=Env.NAME``. Always exposed in
values: Caller-supplied configuration resolved by name when the
workflow YAML references ``=Env.NAME``. Always exposed in
the ``Env`` symbol regardless of ``restrict_to_configuration``.
restrict_to_configuration: When ``True`` (default), the ``Env``
symbol is populated exclusively from ``values``; ``os.environ``
@@ -93,12 +88,35 @@ class DeclarativeEnvConfig:
unrelated environment variables never enter the PowerFx scope.
"""
values: Mapping[str, str] = field(default_factory=lambda: _EMPTY_ENV_VALUES)
values: Mapping[str, str] = field(default_factory=lambda: MappingProxyType({}))
restrict_to_configuration: bool = True
referenced_names: frozenset[str] = _EMPTY_ENV_REFERENCES
referenced_names: frozenset[str] = field(default_factory=lambda: frozenset[str]())
def __post_init__(self) -> None:
# Defensive snapshots so the frozen guarantee extends to the
# contents of ``values`` / ``referenced_names``: caller mutations
# to the original objects after construction cannot leak into
# ``resolve()``.
object.__setattr__(self, "values", MappingProxyType(dict(self.values)))
object.__setattr__(self, "referenced_names", frozenset(self.referenced_names))
_EMPTY_ENV_CONFIG = DeclarativeEnvConfig()
def resolve(self) -> dict[str, str]:
"""Return the resolved ``Env`` symbol mapping for the workflow.
Configuration values are always included (stringified).
``os.environ`` is consulted only when ``restrict_to_configuration``
is ``False`` and the name appears in ``referenced_names``, so
unrelated environment variables never enter the PowerFx scope.
Configuration values always win over the environment fallback.
"""
resolved = {name: str(value) for name, value in self.values.items()}
if self.restrict_to_configuration:
return resolved
for name in self.referenced_names.difference(resolved):
env_value = os.environ.get(name)
if env_value is not None:
resolved[name] = env_value
return resolved
def discover_env_references(node: Any) -> set[str]:
@@ -248,7 +266,7 @@ class DeclarativeWorkflowState:
- Conversation: Conversation history
"""
def __init__(self, state: State, env_config: DeclarativeEnvConfig = _EMPTY_ENV_CONFIG):
def __init__(self, state: State, env_config: DeclarativeEnvConfig | None = None):
"""Initialize with a State instance.
Args:
@@ -259,7 +277,7 @@ class DeclarativeWorkflowState:
matching the safe default of the :class:`WorkflowFactory`.
"""
self._state = state
self._env_config = env_config
self._env_config = env_config if env_config is not None else DeclarativeEnvConfig()
def initialize(self, inputs: Mapping[str, Any] | None = None) -> None:
"""Initialize the declarative state with inputs.
@@ -798,25 +816,12 @@ class DeclarativeWorkflowState:
# Custom namespaces
**state_data.get("Custom", {}),
}
# Populate the ``Env`` symbol from the workflow-level
# :class:`DeclarativeEnvConfig`. Caller-supplied configuration
# values are always exposed (matching .NET ``IConfiguration``
# semantics); ``os.environ`` is consulted only when
# ``restrict_to_configuration`` is ``False`` and the YAML explicitly
# references the name in a PowerFx expression. When both sources
# produce no values the ``Env`` symbol is omitted entirely so
# ``=Env.X`` resolves to the literal expression string (preserving
# the legacy "unbound identifier" fallback behaviour).
env_bound: dict[str, str] = {}
for name, value in self._env_config.values.items():
env_bound[name] = str(value)
if not self._env_config.restrict_to_configuration:
for name in self._env_config.referenced_names:
if name in env_bound:
continue
env_value = os.environ.get(name)
if env_value is not None:
env_bound[name] = env_value
# Resolve the ``Env`` symbol from the workflow-level
# :class:`DeclarativeEnvConfig`. When both ``values`` and the
# ``os.environ`` allowlist produce no entries the symbol is
# omitted so ``=Env.X`` falls back to the literal expression
# string (preserving the legacy "unbound identifier" behaviour).
env_bound = self._env_config.resolve()
if env_bound:
symbols["Env"] = env_bound
# Debug log the Local symbols to help diagnose type issues
@@ -976,7 +981,7 @@ class DeclarativeActionExecutor(Executor):
# executor by :class:`DeclarativeWorkflowBuilder` after construction.
# Defaults to an empty configuration so direct ``DeclarativeActionExecutor``
# construction (e.g. in unit tests) doesn't expose ``os.environ``.
self._declarative_env_config: DeclarativeEnvConfig = _EMPTY_ENV_CONFIG
self._declarative_env_config: DeclarativeEnvConfig = DeclarativeEnvConfig()
# Manually register handlers after initialization
self._handlers = {}
@@ -22,7 +22,6 @@ from agent_framework import (
)
from ._declarative_base import (
_EMPTY_ENV_CONFIG, # type: ignore[reportPrivateUsage]
ConditionResult,
DeclarativeActionExecutor,
DeclarativeEnvConfig,
@@ -178,7 +177,7 @@ class DeclarativeWorkflowBuilder:
self._seen_explicit_ids: set[str] = set() # Track explicit IDs for duplicate detection
self._http_request_handler = http_request_handler
self._mcp_tool_handler = mcp_tool_handler
self._env_config: DeclarativeEnvConfig = env_config if env_config is not None else _EMPTY_ENV_CONFIG
self._env_config: DeclarativeEnvConfig = env_config if env_config is not None else DeclarativeEnvConfig()
# Resolve max_iterations: explicit arg > YAML maxTurns > core default
resolved = max_iterations if max_iterations is not None else yaml_definition.get("maxTurns")
if resolved is not None and (not isinstance(resolved, int) or resolved <= 0):
@@ -124,14 +124,12 @@ class WorkflowFactory:
or auth/connection resolution.
configuration: Optional mapping that populates the PowerFx ``Env``
symbol referenced from workflow YAML expressions (e.g.
``=Env.MY_KEY``). Mirrors the .NET ``IConfiguration``-based
pattern in
``dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/PowerFx/WorkflowDiagnostics.cs``.
Keys supplied here are always exposed under ``Env.<key>``; the
process ``os.environ`` is consulted only when
``restrict_env_to_configuration`` is ``False``. When neither
source produces a value the ``Env`` symbol is omitted so
``=Env.X`` evaluates to the literal expression string.
``=Env.MY_KEY``). Keys supplied here are always exposed
under ``Env.<key>``; the process ``os.environ`` is consulted
only when ``restrict_env_to_configuration`` is ``False``.
When neither source produces a value the ``Env`` symbol is
omitted so ``=Env.X`` evaluates to the literal expression
string.
restrict_env_to_configuration: When ``True`` (default), the
``Env`` PowerFx symbol is populated exclusively from
``configuration``; ``os.environ`` is never consulted. Set to
@@ -198,11 +198,12 @@ class DefaultMCPToolHandler:
"""Reserved ``tool_name`` that maps an :class:`MCPToolHandler` invocation
to the MCP protocol ``tools/list`` discovery operation.
Mirrors the .NET ``DefaultMcpToolHandler.ListToolsToolName`` public
constant for cross-language discoverability. When this handler receives
an invocation with this name it pages through ``session.list_tools()``
and returns the catalog as a single ``TextContent`` containing JSON of
shape ``{"tools": [{name, description, inputSchema, outputSchema}, ...]}``.
The constant matches the underlying MCP method name so a single
string travels unchanged through host code, YAML, and the protocol
wire. When this handler receives an invocation with this name it
pages through ``session.list_tools()`` and returns the catalog as a
single ``TextContent`` containing JSON of shape
``{"tools": [{name, description, inputSchema, outputSchema}, ...]}``.
Workflows can reference this name from an ``InvokeMcpTool`` declarative
action to introspect a server's tool surface without an extra round-trip
from host code.
@@ -237,9 +238,7 @@ class DefaultMCPToolHandler:
intercepted client-side: instead of being forwarded as a tool call,
it is translated to an MCP ``session.list_tools()`` discovery
operation (paginated automatically) and returned as a single
``TextContent`` containing a JSON tool catalog. Matches the .NET
``DefaultMcpToolHandler`` behaviour so the same YAML works
cross-language.
``TextContent`` containing a JSON tool catalog.
"""
from agent_framework import Content
from agent_framework.exceptions import ToolExecutionException
@@ -328,13 +327,12 @@ class DefaultMCPToolHandler:
full catalog as a single ``TextContent`` containing JSON of shape
``{"tools": [{name, description, inputSchema, outputSchema}, ...]}``.
The output shape, property names, and property order match
``DefaultMcpToolHandler.SerializeToolsList`` in the .NET reference
implementation so the same workflow YAML can consume the result
cross-language. ``indent=2`` matches ``Utf8JsonWriter`` ``Indented``
mode; ``allow_nan=False`` guards against producing non-conformant
JSON ``NaN``/``Infinity`` tokens if a misbehaving server returns
such values in a schema.
The output shape, property names, and property order are stable so
downstream PowerFx expressions can rely on the schema. ``indent=2``
produces human-readable JSON for the conversation log;
``allow_nan=False`` guards against producing non-conformant JSON
``NaN``/``Infinity`` tokens if a misbehaving server returns such
values in a schema.
"""
from agent_framework import Content
@@ -644,8 +644,8 @@ class TestListTools:
}
@pytest.mark.asyncio
async def test_list_tools_property_order_matches_dotnet(self) -> None:
"""The JSON property order must match .NET SerializeToolsList: name, description, inputSchema, outputSchema."""
async def test_list_tools_property_order_is_stable(self) -> None:
"""JSON property order is stable: name, description, inputSchema, outputSchema."""
handler = DefaultMCPToolHandler()
with _patch_tool():
await handler.invoke_tool(_invocation())
@@ -662,7 +662,7 @@ class TestListTools:
@pytest.mark.asyncio
async def test_list_tools_indented_output(self) -> None:
"""Output is indented with 2-space indent (matches .NET ``Indented=true``)."""
"""Output is JSON with a 2-space indent so the conversation log is human-readable."""
handler = DefaultMCPToolHandler()
with _patch_tool():
await handler.invoke_tool(_invocation())
@@ -774,6 +774,6 @@ class TestListTools:
assert result.is_error is False
def test_class_attribute_value(self) -> None:
# Constant name MUST be the MCP protocol method name for cross-language parity
# with .NET ``DefaultMcpToolHandler.ListToolsToolName``.
# Constant must equal the MCP protocol method name so a single
# string travels unchanged through host code, YAML, and the wire.
assert DefaultMCPToolHandler.LIST_TOOLS_TOOL_NAME == "tools/list"
@@ -2,19 +2,14 @@
"""Invoke a Foundry toolbox MCP endpoint from a declarative workflow.
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``.
The workflow calls ``microsoft_docs_search`` (the Microsoft Learn Docs
MCP server, bundled into a sample toolbox by ``toolbox_provisioning``)
through the toolbox proxy and asks a Foundry agent to summarise the
result.
Required env vars:
FOUNDRY_PROJECT_ENDPOINT, FOUNDRY_MODEL.
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
"""
@@ -34,25 +29,17 @@ from agent_framework.declarative import (
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 (
FOUNDRY_FEATURES_HEADERS,
build_toolbox_mcp_server_url,
create_sample_toolbox,
)
from toolbox_provisioning import FOUNDRY_FEATURES_HEADERS, create_sample_toolbox
AGENT_NAME = "FoundryToolboxMcpAgent"
TOOLBOX_NAME = "declarative_foundry_toolbox_mcp"
DOCS_SERVER_LABEL = "microsoft_docs"
AGENT_INSTRUCTIONS = """\
You combine results from two tool calls in the conversation:
- ``microsoft_docs_search`` from the Microsoft Learn Docs MCP server
(authoritative Microsoft documentation), and
- ``web_search`` (Foundry built-in) for general web context.
Answer the user's question using ONLY the information present in the
conversation. Prefer Microsoft Learn results for any product or API
question and cite document titles or URLs when available. If neither
result set contains an answer, say so plainly rather than guessing.
Answer the user's question using ONLY the Microsoft Learn docs search
result already present in the conversation. Cite document titles or
URLs when available. If the result does not contain an answer, say so
plainly rather than guessing.
"""
@@ -71,33 +58,18 @@ async def main() -> None:
"""Run the Foundry toolbox MCP workflow."""
project_endpoint = os.environ["FOUNDRY_PROJECT_ENDPOINT"]
model = os.environ["FOUNDRY_MODEL"]
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)
print(f"Provisioning toolbox '{toolbox_name}' in Foundry...")
print(f"Provisioning toolbox '{TOOLBOX_NAME}' in Foundry...")
create_sample_toolbox(
name=toolbox_name,
docs_server_label=docs_server_label,
name=TOOLBOX_NAME,
docs_server_label=DOCS_SERVER_LABEL,
project_endpoint=project_endpoint,
)
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,
)
# 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,
}
toolbox_endpoint = f"{project_endpoint.rstrip('/')}/toolboxes/{TOOLBOX_NAME}/mcp?api-version=v1"
print(f"Toolbox endpoint: {toolbox_endpoint}")
print()
@@ -109,7 +81,7 @@ async def main() -> None:
# 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``.
# default breaks long-running tool calls.
http_client = httpx.AsyncClient(
auth=_BearerAuth(credential),
headers=FOUNDRY_FEATURES_HEADERS,
@@ -136,46 +108,31 @@ async def main() -> None:
factory = WorkflowFactory(
agents={AGENT_NAME: summary_agent},
mcp_tool_handler=mcp_handler,
configuration=workflow_configuration,
configuration={
"FOUNDRY_TOOLBOX_MCP_SERVER_URL": toolbox_endpoint,
"FOUNDRY_TOOLBOX_DOCS_SERVER_LABEL": DOCS_SERVER_LABEL,
},
)
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("Ask a question that can be answered from the Microsoft Learn docs.")
print()
user_input = input("You: ").strip() or "How do I configure logging in the Agent Framework?" # noqa: ASYNC250
# 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
async for event in workflow.run({"text": user_input}, stream=True):
if event.type == "executor_invoked":
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 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 event.executor_id == "search_docs_with_toolbox":
print("[Searching Microsoft Learn docs...]")
elif event.executor_id == "summarize_toolbox_result":
print("[Summarizing results...]")
elif event.type == "output" and isinstance(event.data, str):
if not printed_prefix:
print("\nAgent: ", end="", flush=True)
printed_prefix = True
print(event.data, end="", flush=True)
produced_output = True
if produced_output:
print()
else:
print("\n(no response produced)")
print()
if __name__ == "__main__":
@@ -2,10 +2,10 @@
"""Foundry toolbox provisioning helper for ``invoke_foundry_toolbox_mcp``.
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.
Toolboxes are normally created through the Foundry portal or a separate
deployment script. Bundling the one-off setup here lets the sample run
end-to-end without manual steps. ``main.py`` owns the workflow
execution path.
"""
from collections.abc import Mapping
@@ -23,27 +23,16 @@ from azure.identity import AzureCliCredential
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."""
return f"{project_endpoint.rstrip('/')}/toolboxes/{name}/mcp?api-version={api_version}"
def create_sample_toolbox(
*,
name: str,
docs_server_label: str,
project_endpoint: str,
docs_server_url: str = "https://learn.microsoft.com/api/mcp",
) -> None:
def create_sample_toolbox(*, name: str, docs_server_label: str, project_endpoint: str) -> None:
"""Provision a toolbox version (delete-then-create; idempotent).
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.
Bundles the Microsoft Learn Docs MCP server under ``docs_server_label``.
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.ai.projects.models import MCPTool, Tool
from azure.core.exceptions import ResourceNotFoundError
with (
@@ -57,13 +46,16 @@ def create_sample_toolbox(
pass
tools: list[Tool] = [
MCPTool(server_label=docs_server_label, server_url=docs_server_url, require_approval="never"),
WebSearchTool(),
MCPTool(
server_label=docs_server_label,
server_url="https://learn.microsoft.com/api/mcp",
require_approval="never",
),
]
created = project_client.beta.toolboxes.create_version(
name=name,
description="Sample toolbox combining Microsoft Learn Docs MCP and Foundry web search.",
description="Sample toolbox exposing the Microsoft Learn Docs MCP server.",
tools=tools,
headers=FOUNDRY_FEATURES_HEADERS,
)
@@ -1,31 +1,12 @@
#
# This workflow demonstrates the InvokeMcpTool action against a Foundry
# toolbox MCP proxy that exposes BOTH a built-in Foundry tool
# (``web_search``) and an external MCP server (Microsoft Learn Docs)
# behind a single MCP-compatible endpoint.
# Calls the Microsoft Learn Docs MCP server through a Foundry toolbox
# proxy from a declarative workflow, then asks a Foundry agent to
# summarise the result. The toolbox surfaces MCP-server-backed tools
# as ``<server_label>___<tool_name>``.
#
# The workflow:
# 1. Accepts a documentation / web search query as input.
# 2. Lists the tools exposed by the toolbox using the reserved
# toolName: ``tools/list``. ``DefaultMCPToolHandler`` intercepts
# this reserved name natively and translates it
# to an MCP ``session.list_tools()`` call, returning a JSON catalog.
# 3. Invokes the Microsoft Learn ``microsoft_docs_search`` MCP tool
# surfaced by the toolbox. Tool names from MCP-server-backed
# toolbox tools are namespaced as ``<server_label>___<tool_name>``.
# 4. Invokes the built-in ``web_search`` tool through the same
# toolbox proxy. Note: ``web_search`` expects ``search_query``
# (not ``query``).
# 5. Asks a Foundry agent to combine the two result sets in the
# conversation and answer the user's question.
#
# Workflow inputs (set by the host via ``workflow.run({...})``):
# Workflow inputs:
# text: The user's question (required).
#
# Example inputs:
# How do I configure logging in the Agent Framework?
# What is Azure AI Foundry?
#
kind: Workflow
trigger:
@@ -33,37 +14,14 @@ trigger:
id: workflow_invoke_foundry_toolbox_mcp
actions:
# Set the search query from the workflow input so each MCP tool
# call can pass it as an argument.
- kind: SetVariable
id: set_search_query
variable: Local.SearchQuery
value: =Workflow.Inputs.text
# 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
serverLabel: foundry_toolbox
toolName: tools/list
headers:
Foundry-Features: Toolboxes=V1Preview
output:
autoSend: false
result: Local.ToolboxTools
# Invoke ``microsoft_docs_search`` from the Microsoft Learn MCP
# server. The toolbox prefixes MCP-server tools with the server
# label declared at toolbox-creation time.
#
# ``autoSend: false`` suppresses dumping the raw JSON result to the
# workflow output stream — the result is still parsed into
# ``Local.SearchResult`` AND appended to the conversation (via
# ``conversationId``) so the downstream agent can summarise it.
# ``autoSend: false`` so the raw JSON tool result is not echoed to
# the host's output stream; ``conversationId`` still appends it to
# the conversation so the summarising agent can read it.
- kind: InvokeMcpTool
id: search_docs_with_toolbox
serverUrl: =Env.FOUNDRY_TOOLBOX_MCP_SERVER_URL
@@ -78,35 +36,13 @@ trigger:
autoSend: false
result: Local.SearchResult
# Invoke the built-in ``web_search`` tool through the same toolbox
# proxy. ``web_search`` is a Foundry built-in (not an MCP server),
# so it is NOT namespaced and expects the argument
# ``search_query`` (not ``query``). See the docs_search action
# above for why ``autoSend: false`` is used here.
- kind: InvokeMcpTool
id: search_web_with_toolbox
serverUrl: =Env.FOUNDRY_TOOLBOX_MCP_SERVER_URL
serverLabel: foundry_toolbox
toolName: =Env.FOUNDRY_TOOLBOX_WEB_SEARCH_TOOL_NAME
conversationId: =System.ConversationId
headers:
Foundry-Features: Toolboxes=V1Preview
arguments:
search_query: =Local.SearchQuery
output:
autoSend: false
result: Local.WebSearchResult
# Ask the agent to summarise the two toolbox results. The agent
# reads the prior conversation (which now contains both result
# sets via ``conversationId``) and produces a single answer.
- kind: InvokeAzureAgent
id: summarize_toolbox_result
agent:
name: FoundryToolboxMcpAgent
conversationId: =System.ConversationId
input:
messages: =Concat("Combine the Microsoft Learn docs results and the Foundry web search results in the conversation to answer the query ", Local.SearchQuery)
messages: '=Concat("Answer the query using the Microsoft Learn docs result already in the conversation: ", Local.SearchQuery)'
output:
autoSend: true
messages: Local.Summary