mirror of
https://github.com/microsoft/agent-framework.git
synced 2026-06-16 21:04:09 +08:00
Revamped sample to address PR comments.
This commit is contained in:
@@ -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__":
|
||||
|
||||
+16
-24
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user