Files
microsoft--agent-framework/python/samples/02-agents/mcp/mcp_progressive_disclosure.py
T
wehub-resource-sync db620d33df
CodeQL / Analyze (csharp) (push) Waiting to run
CodeQL / Analyze (python) (push) Waiting to run
dotnet-build-and-test / dotnet-test-functions (push) Has been cancelled
dotnet-build-and-test / paths-filter (push) Has been cancelled
dotnet-build-and-test / dotnet-build (Debug, windows-latest, net9.0) (push) Has been cancelled
dotnet-build-and-test / dotnet-build (Release, ubuntu-latest, net10.0) (push) Has been cancelled
dotnet-build-and-test / dotnet-build (Release, ubuntu-latest, net8.0) (push) Has been cancelled
dotnet-build-and-test / dotnet-build (Release, windows-latest, net472) (push) Has been cancelled
dotnet-build-and-test / dotnet-test (Release, integration, true, ubuntu-latest, net10.0) (push) Has been cancelled
dotnet-build-and-test / dotnet-test (Release, integration, true, windows-latest, net472) (push) Has been cancelled
dotnet-build-and-test / dotnet-foundry-hosted-it (push) Has been cancelled
dotnet-build-and-test / dotnet-build-and-test-check (push) Has been cancelled
dotnet-build-and-test / Integration Test Report (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:39:25 +08:00

155 lines
6.5 KiB
Python

# Copyright (c) Microsoft. All rights reserved.
import asyncio
import sys
from typing import Any
from agent_framework import Agent, MCPStdioTool
from agent_framework.openai import OpenAIChatClient
from dotenv import load_dotenv
__doc__ = """
MCP Progressive Disclosure Example
This sample demonstrates how to connect an agent to a large MCP server without
frontloading every remote tool schema into the model prompt.
The sample starts a tiny local MCP stdio server in a child process. The server
advertises three tools:
1. ``get_server_status`` — always visible to the model.
2. ``search_docs`` — allowed, but hidden until the model calls ``docs_load_tool`` and removable with
``docs_unload_tool`` when it is no longer useful.
3. ``internal_admin_report`` — not listed in ``allowed_tools``, so the model never
sees it in ``docs_list_mcp_tools`` and cannot load it.
The ``MCPStdioTool`` is configured with:
1. ``use_progressive_disclosure=True`` to enable loader tools.
2. ``always_load=["get_server_status"]`` to keep one cheap tool visible up front.
3. ``allowed_tools=[...]`` to define the only remote tools the model may discover
or load.
4. ``tool_name_prefix="docs"`` so multiple MCP servers can expose their own
``docs_list_mcp_tools`` / ``docs_load_tool`` / ``docs_unload_tool`` names without collisions.
``docs_load_tool`` and ``docs_unload_tool`` accept either one tool name or a list of tool names.
Sample output:
User: Explain how progressive MCP tool disclosure works. First inspect the MCP
tools you can load, then load the docs search tool, use it, and unload it.
Agent: Progressive disclosure starts with a small set of tools. I listed the
available MCP tools, loaded docs_search_docs, and used it to find that hidden
MCP tools become available on the next function-calling iteration.
"""
load_dotenv()
async def _run_server() -> None:
"""Run a minimal stdio MCP server with visible, loadable, and filtered tools."""
import mcp.types as types
from mcp.server.lowlevel import Server
from mcp.server.stdio import stdio_server
server: Server[Any, Any] = Server("mcp-progressive-disclosure-demo")
@server.list_tools()
async def _list_tools() -> list[types.Tool]: # pyright: ignore[reportUnusedFunction]
return [
types.Tool(
name="get_server_status",
description="Return the health of the demo MCP server.",
inputSchema={"type": "object", "properties": {}},
),
types.Tool(
name="search_docs",
description="Search short documentation snippets about MCP progressive disclosure.",
inputSchema={
"type": "object",
"properties": {
"query": {
"type": "string",
"description": "The documentation search query.",
}
},
"required": ["query"],
},
),
types.Tool(
name="internal_admin_report",
description="Internal server details that are intentionally filtered out by allowed_tools.",
inputSchema={"type": "object", "properties": {}},
),
]
@server.call_tool()
async def _call_tool(name: str, arguments: dict[str, Any]) -> types.CallToolResult: # pyright: ignore[reportUnusedFunction]
if name == "get_server_status":
text = "The demo MCP server is healthy. Use search_docs for progressive disclosure details."
elif name == "search_docs":
query = str(arguments.get("query", "")).strip() or "progressive disclosure"
text = (
f"Search results for '{query}': In progressive MCP disclosure, the agent starts with "
"list/load/unload tools and selected always-loaded tools. Calling load_tool adds an allowed "
"remote MCP tool to the live tool list for the next model iteration, and unload_tool removes it."
)
elif name == "internal_admin_report":
text = "This tool should not be discoverable because it is excluded by allowed_tools."
else:
text = f"Unknown tool: {name}"
return types.CallToolResult(content=[types.TextContent(type="text", text=text)])
async with stdio_server() as (read_stream, write_stream):
await server.run(read_stream, write_stream, server.create_initialization_options())
async def _run_client() -> None:
"""Run an agent that progressively discovers and loads MCP tools."""
# 1. Create the MCP tool. Only get_server_status is visible at first; search_docs
# is discoverable through docs_list_mcp_tools, loadable through docs_load_tool,
# and unloadable through docs_unload_tool.
mcp_tool = MCPStdioTool(
name="DocsMCP",
description="Demo MCP server with progressively loaded documentation tools.",
command=sys.executable,
args=[__file__, "--server"],
allowed_tools=["get_server_status", "search_docs"],
use_progressive_disclosure=True,
always_load=["get_server_status"],
tool_name_prefix="docs",
)
# 2. Create an agent with the progressive MCP tool.
async with Agent(
client=OpenAIChatClient(),
name="ProgressiveMCPAgent",
instructions=(
"You are a helpful assistant. To answer documentation questions, first call "
"docs_list_mcp_tools to see which MCP tools are available. If you need a "
"hidden tool, call docs_load_tool with that tool's remote name, then call "
"the newly available prefixed tool on the next iteration. When the hidden "
"tool is no longer needed, call docs_unload_tool. Do not invent tools that are not listed."
),
tools=mcp_tool,
) as agent:
# 3. Ask a question that requires loading a hidden MCP tool.
prompt = (
"Explain how progressive MCP tool disclosure works. First inspect the MCP "
"tools you can load, then load the docs search tool, use it, and unload it."
)
print(f"User: {prompt}")
response = await agent.run(prompt)
print(f"Agent: {response.text}")
async def main() -> None:
"""Run either the MCP server branch or the agent client branch."""
if "--server" in sys.argv:
await _run_server()
else:
await _run_client()
if __name__ == "__main__":
asyncio.run(main())