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

This commit is contained in:
wehub-resource-sync
2026-07-13 13:39:25 +08:00
commit db620d33df
5151 changed files with 925932 additions and 0 deletions
+37
View File
@@ -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())