# 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())