chore: import upstream snapshot with attribution
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
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
This commit is contained in:
@@ -0,0 +1,37 @@
|
||||
# MCP (Model Context Protocol) Examples
|
||||
|
||||
This folder contains examples demonstrating how to work with MCP using Agent Framework.
|
||||
|
||||
## What is MCP?
|
||||
|
||||
The Model Context Protocol (MCP) is an open standard for connecting AI agents to data sources and tools. It enables secure, controlled access to local and remote resources through a standardized protocol.
|
||||
|
||||
## Examples
|
||||
|
||||
| Sample | File | Description |
|
||||
|--------|------|-------------|
|
||||
| **Agent as MCP Server** | [`agent_as_mcp_server.py`](agent_as_mcp_server.py) | Shows how to expose an Agent Framework agent as an MCP server that other AI applications can connect to |
|
||||
| **API Key Authentication** | [`mcp_api_key_auth.py`](mcp_api_key_auth.py) | Demonstrates API key authentication with MCP servers using `header_provider`, runtime invocation kwargs, and a command-line API key argument |
|
||||
| **GitHub Integration with PAT** | [`mcp_github_pat.py`](mcp_github_pat.py) | Demonstrates connecting to GitHub's MCP server using Personal Access Token (PAT) authentication |
|
||||
| **Long-Running Task** | [`mcp_long_running_task.py`](mcp_long_running_task.py) | Demonstrates transparent SEP-2663 long-running task handling for MCP tools that advertise `taskSupport=required`. Self-spawns a stdio MCP child server |
|
||||
| **Progressive Disclosure** | [`mcp_progressive_disclosure.py`](mcp_progressive_disclosure.py) | Demonstrates `use_progressive_disclosure`, `always_load`, `allowed_tools`, and prefixed `list_mcp_tools` / `load_tool` / `unload_tool` names. `load_tool` and `unload_tool` can accept one tool name or multiple names. Self-spawns a stdio MCP child server |
|
||||
| **Sampling Approval** | [`mcp_sampling_approval.py`](mcp_sampling_approval.py) | Demonstrates gating server-initiated `sampling/createMessage` requests with a `sampling_approval_callback`, plus the `sampling_max_tokens` and `sampling_max_requests` guardrails. MCP sampling is denied by default |
|
||||
|
||||
## Prerequisites
|
||||
|
||||
Most samples in this folder use OpenAI:
|
||||
|
||||
- `OPENAI_API_KEY` environment variable
|
||||
- `OPENAI_CHAT_MODEL` environment variable
|
||||
|
||||
Run `mcp_api_key_auth.py` with the MCP API key as the first command-line argument.
|
||||
|
||||
`mcp_progressive_disclosure.py` self-spawns its demo MCP stdio server; no separate MCP server setup is required.
|
||||
|
||||
For `mcp_github_pat.py`:
|
||||
- `GITHUB_PAT` - Your GitHub Personal Access Token (create at https://github.com/settings/tokens)
|
||||
|
||||
For `mcp_long_running_task.py` (uses Azure OpenAI via Entra-ID):
|
||||
- Run `az login` once
|
||||
- `AZURE_OPENAI_ENDPOINT` - your Azure OpenAI resource endpoint, e.g. `https://<resource>.openai.azure.com/`
|
||||
- `AZURE_OPENAI_CHAT_MODEL` (or `AZURE_OPENAI_MODEL`) - the deployment name (e.g. `gpt-4o-mini`)
|
||||
@@ -0,0 +1,80 @@
|
||||
# Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
from typing import Annotated, Any
|
||||
|
||||
import anyio
|
||||
from agent_framework import Agent, tool
|
||||
from agent_framework.openai import OpenAIChatClient
|
||||
from dotenv import load_dotenv
|
||||
|
||||
# Load environment variables from .env file
|
||||
load_dotenv()
|
||||
|
||||
"""
|
||||
This sample demonstrates how to expose an Agent as an MCP server.
|
||||
|
||||
To run this sample, set up your MCP host (like Claude Desktop or VSCode GitHub Copilot Agents)
|
||||
with the following configuration:
|
||||
```json
|
||||
{
|
||||
"servers": {
|
||||
"agent-framework": {
|
||||
"command": "uv",
|
||||
"args": [
|
||||
"--directory=<path to project>/agent-framework/python/samples/02-agents/mcp",
|
||||
"run",
|
||||
"agent_as_mcp_server.py"
|
||||
],
|
||||
"env": {
|
||||
"OPENAI_API_KEY": "<OpenAI API key>",
|
||||
"OPENAI_MODEL": "<OpenAI Responses model ID>",
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
"""
|
||||
|
||||
|
||||
# NOTE: approval_mode="never_require" is for sample brevity. Use "always_require" in production; see samples/02-agents/tools/function_tool_with_approval.py and samples/02-agents/tools/function_tool_with_approval_and_sessions.py.
|
||||
@tool(approval_mode="never_require")
|
||||
def get_specials() -> Annotated[str, "Returns the specials from the menu."]:
|
||||
return """
|
||||
Special Soup: Clam Chowder
|
||||
Special Salad: Cobb Salad
|
||||
Special Drink: Chai Tea
|
||||
"""
|
||||
|
||||
|
||||
@tool(approval_mode="never_require")
|
||||
def get_item_price(
|
||||
menu_item: Annotated[str, "The name of the menu item."],
|
||||
) -> Annotated[str, "Returns the price of the menu item."]:
|
||||
return "$9.99"
|
||||
|
||||
|
||||
async def run() -> None:
|
||||
# Define an agent
|
||||
# Agent's name and description provide better context for AI model
|
||||
agent = Agent(
|
||||
client=OpenAIChatClient(),
|
||||
name="RestaurantAgent",
|
||||
description="Answer questions about the menu.",
|
||||
tools=[get_specials, get_item_price],
|
||||
)
|
||||
|
||||
# Expose the agent as an MCP server
|
||||
server = agent.as_mcp_server()
|
||||
|
||||
# Run server
|
||||
from mcp.server.stdio import stdio_server
|
||||
|
||||
async def handle_stdin(stdin: Any | None = None, stdout: Any | None = None) -> None:
|
||||
async with stdio_server() as (read_stream, write_stream):
|
||||
await server.run(read_stream, write_stream, server.create_initialization_options())
|
||||
|
||||
await handle_stdin()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
anyio.run(run)
|
||||
@@ -0,0 +1,60 @@
|
||||
# Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
import asyncio
|
||||
import sys
|
||||
|
||||
from agent_framework import Agent, MCPStreamableHTTPTool
|
||||
from agent_framework.openai import OpenAIChatClient
|
||||
from dotenv import load_dotenv
|
||||
|
||||
# Load environment variables from .env file
|
||||
load_dotenv()
|
||||
|
||||
"""
|
||||
MCP API Key Authentication Example
|
||||
|
||||
This sample demonstrates the runtime ``header_provider`` pattern for
|
||||
``MCPStreamableHTTPTool``. The MCP tool derives authentication headers from
|
||||
``function_invocation_kwargs`` passed to ``Agent.run(...)`` so the API key stays
|
||||
in runtime context instead of being baked into a shared ``httpx.AsyncClient``.
|
||||
|
||||
Replace the ``url`` parameter in the ``MCPStreamableHTTPTool`` with your authenticated server URL and
|
||||
run the sample with your API key as a command-line argument:
|
||||
python mcp_api_key_auth.py <your_api_key>
|
||||
|
||||
The ``header_provider`` here is just a simple lambda, but it can be a more complex function that retrieves and
|
||||
formats headers as needed, allowing for flexible authentication schemes.
|
||||
For more complex scenarios, you could implement token refresh logic or support multiple authentication methods
|
||||
within the header provider function.
|
||||
|
||||
For more authentication examples including OAuth 2.0 flows, see:
|
||||
- https://github.com/modelcontextprotocol/python-sdk/tree/main/examples/clients/simple-auth-client
|
||||
- https://github.com/modelcontextprotocol/python-sdk/tree/main/examples/servers/simple-auth
|
||||
"""
|
||||
|
||||
|
||||
async def api_key_auth_example(api_key: str) -> None:
|
||||
"""Run an agent against an MCP server using runtime-provided API key headers."""
|
||||
|
||||
async with Agent(
|
||||
client=OpenAIChatClient(),
|
||||
name="Agent",
|
||||
instructions="You are a helpful assistant. Use your MCP tool when answering the user's question.",
|
||||
tools=MCPStreamableHTTPTool(
|
||||
name="MCP tool",
|
||||
description="MCP tool description.",
|
||||
url="<your authenticated server url>",
|
||||
header_provider=lambda kwargs: {"Authorization": f"Bearer {kwargs['mcp_api_key']}"},
|
||||
),
|
||||
) as agent:
|
||||
query = "Use your MCP tool to tell me what tools are available to you."
|
||||
print(f"User: {query}")
|
||||
result = await agent.run(
|
||||
query,
|
||||
function_invocation_kwargs={"mcp_api_key": api_key},
|
||||
)
|
||||
print(f"Agent: {result.text}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(api_key_auth_example(sys.argv[1]))
|
||||
@@ -0,0 +1,83 @@
|
||||
# Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
import asyncio
|
||||
import os
|
||||
|
||||
from agent_framework import Agent
|
||||
from agent_framework.openai import OpenAIChatClient
|
||||
from dotenv import load_dotenv
|
||||
|
||||
"""
|
||||
MCP GitHub Integration with Personal Access Token (PAT)
|
||||
|
||||
This example demonstrates how to connect to GitHub's remote MCP server using a Personal Access
|
||||
Token (PAT) for authentication. The agent can use GitHub operations like searching repositories,
|
||||
reading files, creating issues, and more depending on how you scope your token.
|
||||
|
||||
Prerequisites:
|
||||
1. A GitHub Personal Access Token with appropriate scopes
|
||||
- Create one at: https://github.com/settings/tokens
|
||||
- For read-only operations, you can use more restrictive scopes
|
||||
2. Environment variables:
|
||||
- GITHUB_PAT: Your GitHub Personal Access Token (required)
|
||||
- OPENAI_API_KEY: Your OpenAI API key (required)
|
||||
- OPENAI_MODEL: Your OpenAI model ID (required)
|
||||
"""
|
||||
|
||||
|
||||
async def github_mcp_example() -> None:
|
||||
"""Example of using GitHub MCP server with PAT authentication."""
|
||||
# 1. Load environment variables from .env file if present
|
||||
load_dotenv()
|
||||
|
||||
# 2. Get configuration from environment
|
||||
github_pat = os.getenv("GITHUB_PAT")
|
||||
if not github_pat:
|
||||
raise ValueError(
|
||||
"GITHUB_PAT environment variable must be set. Create a token at https://github.com/settings/tokens"
|
||||
)
|
||||
|
||||
# 3. Create authentication headers with GitHub PAT
|
||||
auth_headers = {
|
||||
"Authorization": f"Bearer {github_pat}",
|
||||
}
|
||||
|
||||
# 4. Create agent with the GitHub MCP tool using instance method
|
||||
# The MCP tool manages the connection to the MCP server and makes its tools available
|
||||
# Set approval_mode="never_require" to allow the MCP tool to execute without approval
|
||||
client = OpenAIChatClient()
|
||||
# Note that the tool created here will be executed remotely by OpenAI, not locally by
|
||||
# your application.
|
||||
github_mcp_tool = client.get_mcp_tool(
|
||||
name="GitHub",
|
||||
url="https://api.githubcopilot.com/mcp/",
|
||||
headers=auth_headers,
|
||||
approval_mode="never_require",
|
||||
)
|
||||
|
||||
# 5. Create agent with the GitHub MCP tool
|
||||
async with Agent(
|
||||
client=client,
|
||||
name="GitHubAgent",
|
||||
instructions=(
|
||||
"You are a helpful assistant that can help users interact with GitHub. "
|
||||
"You can search for repositories, read file contents, check issues, and more. "
|
||||
"Always be clear about what operations you're performing."
|
||||
),
|
||||
tools=github_mcp_tool,
|
||||
) as agent:
|
||||
# Example 1: Get authenticated user information
|
||||
query1 = "What is my GitHub username and tell me about my account?"
|
||||
print(f"\nUser: {query1}")
|
||||
result1 = await agent.run(query1)
|
||||
print(f"Agent: {result1.text}")
|
||||
|
||||
# Example 2: List my repositories
|
||||
query2 = "List all the repositories I own on GitHub"
|
||||
print(f"\nUser: {query2}")
|
||||
result2 = await agent.run(query2)
|
||||
print(f"Agent: {result2.text}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(github_mcp_example())
|
||||
@@ -0,0 +1,181 @@
|
||||
# Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
"""
|
||||
MCP Long-Running Task (SEP-2663) Example
|
||||
|
||||
Demonstrates that ``MCPStdioTool`` transparently drives the MCP long-running
|
||||
task lifecycle for tools that advertise ``execution.taskSupport == "required"``.
|
||||
The agent observes a single function-call result; the framework handles the
|
||||
``tools/call`` → ``tasks/get`` (polled) → ``tasks/result`` sequence in the
|
||||
background.
|
||||
|
||||
Run it as a single file. The script doubles as both the client and the stdio
|
||||
MCP child server (the child branch is selected via ``--server``):
|
||||
|
||||
python mcp_long_running_task.py
|
||||
|
||||
Requirements:
|
||||
- Azure CLI sign-in (``az login``) — used for Entra-ID auth against Azure OpenAI.
|
||||
- ``AZURE_OPENAI_ENDPOINT`` — your Azure OpenAI resource endpoint, e.g.
|
||||
``https://<resource>.openai.azure.com/``.
|
||||
- ``AZURE_OPENAI_CHAT_MODEL`` (or ``AZURE_OPENAI_MODEL``) — the deployment name,
|
||||
e.g. ``gpt-4o-mini``.
|
||||
|
||||
This sample uses the lower-level ``mcp.server.lowlevel.Server`` so it can:
|
||||
1. Advertise a tool with ``execution=ToolExecution(taskSupport="required")``.
|
||||
2. Enable the SDK's experimental task support for the ``tasks/*`` lifecycle.
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import sys
|
||||
from datetime import timedelta
|
||||
from typing import Any
|
||||
|
||||
from agent_framework import Agent, MCPStdioTool, MCPTaskOptions
|
||||
from agent_framework.openai import OpenAIChatClient
|
||||
from azure.identity import AzureCliCredential
|
||||
from dotenv import load_dotenv
|
||||
|
||||
load_dotenv()
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# MCP stdio server (child-process branch)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
async def _run_server() -> None:
|
||||
"""Run a minimal stdio MCP server exposing one long-running tool."""
|
||||
import mcp.types as types
|
||||
from mcp.server.lowlevel import Server
|
||||
from mcp.server.stdio import stdio_server
|
||||
|
||||
server: Server[Any, Any] = Server("mcp-long-running-task-demo")
|
||||
# Auto-registers handlers for tasks/get, tasks/result, tasks/cancel, tasks/list
|
||||
# backed by an in-memory store.
|
||||
server.experimental.enable_tasks()
|
||||
|
||||
@server.list_tools()
|
||||
async def _list_tools() -> list[types.Tool]: # pyright: ignore[reportUnusedFunction]
|
||||
return [
|
||||
types.Tool(
|
||||
name="slow_summary",
|
||||
description=(
|
||||
"Produces a short summary of the supplied text after simulating several seconds of expensive work."
|
||||
),
|
||||
inputSchema={
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"text": {
|
||||
"type": "string",
|
||||
"description": "Text to summarize.",
|
||||
}
|
||||
},
|
||||
"required": ["text"],
|
||||
},
|
||||
# Advertise that this tool MUST be invoked via the task lifecycle.
|
||||
execution=types.ToolExecution(taskSupport="required"),
|
||||
)
|
||||
]
|
||||
|
||||
@server.call_tool()
|
||||
async def _call_tool(name: str, arguments: dict[str, Any]) -> Any: # pyright: ignore[reportUnusedFunction]
|
||||
if name != "slow_summary":
|
||||
raise ValueError(f"Unknown tool: {name}")
|
||||
|
||||
ctx = server.request_context
|
||||
|
||||
async def _work(task: Any) -> types.CallToolResult:
|
||||
await task.update_status("Thinking...")
|
||||
await asyncio.sleep(15.0)
|
||||
text: str = (arguments.get("text") or "").strip()
|
||||
words = text.split()
|
||||
preview = " ".join(words[:6]) + ("..." if len(words) > 6 else "")
|
||||
summary = (
|
||||
f"Summarized {len(words)} word(s). First few words: '{preview}'."
|
||||
if words
|
||||
else "No input text was provided."
|
||||
)
|
||||
return types.CallToolResult(
|
||||
content=[types.TextContent(type="text", text=summary)],
|
||||
isError=False,
|
||||
)
|
||||
|
||||
if not ctx.experimental.is_task:
|
||||
# Client invoked the tool without task augmentation. Return a hard
|
||||
# error so a misconfigured client surfaces the problem clearly.
|
||||
return types.CallToolResult(
|
||||
content=[
|
||||
types.TextContent(
|
||||
type="text",
|
||||
text="'slow_summary' must be invoked as a task.",
|
||||
)
|
||||
],
|
||||
isError=True,
|
||||
)
|
||||
|
||||
return await ctx.experimental.run_task(_work)
|
||||
|
||||
async with stdio_server() as (read_stream, write_stream):
|
||||
await server.run(read_stream, write_stream, server.create_initialization_options())
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Agent client (default branch)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
async def _run_client() -> None:
|
||||
mcp_tool = MCPStdioTool(
|
||||
name="LongRunningDemo",
|
||||
description="Demo MCP server exposing a tool that advertises taskSupport=required.",
|
||||
command=sys.executable,
|
||||
args=[__file__, "--server"],
|
||||
# Optional: cap individual tasks at two minutes. The server may apply its
|
||||
# own default if this is omitted.
|
||||
task_options=MCPTaskOptions(default_ttl=timedelta(minutes=2)),
|
||||
)
|
||||
|
||||
async with Agent(
|
||||
client=OpenAIChatClient(credential=AzureCliCredential()),
|
||||
name="LROAgent",
|
||||
instructions=(
|
||||
"You are a helpful assistant. Use the slow_summary tool when the user "
|
||||
"asks for a summary. Wait for the result and present it directly."
|
||||
),
|
||||
tools=mcp_tool,
|
||||
) as agent:
|
||||
prompt = (
|
||||
"Please summarize the following text using your slow_summary tool: "
|
||||
"'The Model Context Protocol lets language models talk to external "
|
||||
"tools and resources through a small JSON-RPC surface.'"
|
||||
)
|
||||
|
||||
print("=== run() ===")
|
||||
print(f"User: {prompt}")
|
||||
response = await agent.run(prompt)
|
||||
print(f"Agent: {response.text}\n")
|
||||
|
||||
print("=== run(stream=True) ===")
|
||||
print(f"User: {prompt}")
|
||||
print("Agent: ", end="", flush=True)
|
||||
async for update in agent.run(prompt, stream=True):
|
||||
if update.text:
|
||||
print(update.text, end="", flush=True)
|
||||
print()
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Entry point
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def main() -> None:
|
||||
if len(sys.argv) > 1 and sys.argv[1] == "--server":
|
||||
asyncio.run(_run_server())
|
||||
return
|
||||
asyncio.run(_run_client())
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,154 @@
|
||||
# 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())
|
||||
@@ -0,0 +1,78 @@
|
||||
# Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
import asyncio
|
||||
|
||||
from agent_framework import Agent, MCPStreamableHTTPTool
|
||||
from agent_framework.openai import OpenAIChatClient
|
||||
from dotenv import load_dotenv
|
||||
from mcp import types
|
||||
|
||||
# Load environment variables from .env file
|
||||
load_dotenv()
|
||||
|
||||
"""
|
||||
MCP Sampling Approval Example
|
||||
|
||||
MCP servers can send the client a ``sampling/createMessage`` request, asking the
|
||||
client to run an LLM completion on the server's behalf. Because remote MCP
|
||||
servers are untrusted third parties, forwarding these server-controlled prompts
|
||||
to your chat client without review is a confused-deputy risk: a malicious server
|
||||
could exfiltrate context, force tool calls, or burn through your token budget.
|
||||
|
||||
For that reason Agent Framework **denies MCP sampling by default**. To allow it,
|
||||
pass a ``sampling_approval_callback`` to the MCP tool. The callback receives the
|
||||
raw ``CreateMessageRequestParams`` and returns ``True`` to approve or ``False``
|
||||
to deny. It may be synchronous or asynchronous, so you can implement a
|
||||
human-in-the-loop prompt, a policy check, or an audit log.
|
||||
|
||||
Two further guardrails apply to approved requests:
|
||||
- ``sampling_max_tokens`` caps the server-requested ``maxTokens``.
|
||||
- ``sampling_max_requests`` limits how many sampling requests a single session
|
||||
may make.
|
||||
|
||||
To restore the legacy "always approve" behavior (only do this for servers you
|
||||
trust), pass ``sampling_approval_callback=lambda params: True``.
|
||||
"""
|
||||
|
||||
|
||||
async def approve_sampling(params: types.CreateMessageRequestParams) -> bool:
|
||||
"""Human-in-the-loop approval gate for server-initiated sampling.
|
||||
|
||||
Shows the server-supplied system prompt and messages, then asks the user to
|
||||
approve or deny. Returning ``False`` rejects the request.
|
||||
"""
|
||||
print("\n--- MCP server requested a sampling/createMessage ---")
|
||||
if params.systemPrompt:
|
||||
print(f"System prompt: {params.systemPrompt}")
|
||||
for message in params.messages:
|
||||
text = getattr(message.content, "text", message.content)
|
||||
print(f"{message.role}: {text}")
|
||||
answer = await asyncio.to_thread(input, "Approve this sampling request? [y/N]: ")
|
||||
return answer.strip().lower() in {"y", "yes"}
|
||||
|
||||
|
||||
async def main() -> None:
|
||||
"""Run an agent against an MCP server with a sampling approval gate."""
|
||||
async with Agent(
|
||||
client=OpenAIChatClient(),
|
||||
name="Agent",
|
||||
instructions="You are a helpful assistant. Use your MCP tool when answering the user's question.",
|
||||
tools=MCPStreamableHTTPTool(
|
||||
name="MCP tool",
|
||||
description="MCP tool description.",
|
||||
url="<your mcp server url>",
|
||||
# Passing ``client`` enables sampling; the approval callback gates it.
|
||||
client=OpenAIChatClient(),
|
||||
sampling_approval_callback=approve_sampling,
|
||||
sampling_max_tokens=2048,
|
||||
sampling_max_requests=5,
|
||||
),
|
||||
) as agent:
|
||||
query = "Use your MCP tool to help answer this question."
|
||||
print(f"User: {query}")
|
||||
result = await agent.run(query)
|
||||
print(f"Agent: {result.text}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
Reference in New Issue
Block a user