chore: import upstream snapshot with attribution
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) Has been cancelled
CodeQL / Analyze (python) (push) Has been cancelled
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) Has been cancelled
CodeQL / Analyze (python) (push) Has been cancelled
This commit is contained in:
+25
@@ -0,0 +1,25 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFrameworks>net10.0</TargetFrameworks>
|
||||
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<NoWarn>$(NoWarn);MAAI001;MEAI001;MCPEXP001</NoWarn>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Azure.AI.OpenAI" />
|
||||
<PackageReference Include="Azure.Identity" />
|
||||
<PackageReference Include="Microsoft.Extensions.AI.OpenAI" />
|
||||
<PackageReference Include="Microsoft.Extensions.Hosting" />
|
||||
<PackageReference Include="ModelContextProtocol" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\..\..\src\Microsoft.Agents.AI.Mcp\Microsoft.Agents.AI.Mcp.csproj" />
|
||||
<ProjectReference Include="..\..\..\..\src\Microsoft.Agents.AI.OpenAI\Microsoft.Agents.AI.OpenAI.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
+145
@@ -0,0 +1,145 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
// This sample demonstrates the Microsoft Agent Framework's MCP long-running task support.
|
||||
//
|
||||
// A small MCP server (hosted in this same executable when launched with "--server") exposes
|
||||
// a single task-supporting tool "AnalyzeDataset" that simulates ~15 seconds of work. The
|
||||
// client (default mode) connects to it over stdio via Microsoft.Agents.AI.Mcp's
|
||||
// McpClientTaskExtensions.ListAgentToolsWithTaskSupportAsync, hands the wrapped tools to a
|
||||
// ChatClientAgent, and exercises both invocation styles:
|
||||
// * RunAsync — blocks until the agent's final response is ready.
|
||||
// * RunStreamingAsync — yields response updates as the model produces them; the model
|
||||
// still waits for the tool's terminal result before it can begin
|
||||
// producing the final answer, so the perceived "pause" reflects
|
||||
// tool execution time, not stream-channel latency.
|
||||
//
|
||||
// In both cases the wrapper transparently:
|
||||
// 1. Calls tools/call with task augmentation (CallToolAsTaskAsync)
|
||||
// 2. Polls tasks/get until terminal (PollTaskUntilCompleteAsync)
|
||||
// 3. Fetches tasks/result and returns the final result to the function-calling loop
|
||||
//
|
||||
// No application-level loop or continuation tokens are required in either mode.
|
||||
|
||||
using System.ComponentModel;
|
||||
using Azure.AI.OpenAI;
|
||||
using Azure.Identity;
|
||||
using Microsoft.Agents.AI;
|
||||
using Microsoft.Agents.AI.Mcp;
|
||||
using Microsoft.Extensions.AI;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using ModelContextProtocol;
|
||||
using ModelContextProtocol.Client;
|
||||
using ModelContextProtocol.Protocol;
|
||||
using ModelContextProtocol.Server;
|
||||
using OpenAI.Chat;
|
||||
|
||||
if (args.Length > 0 && args[0] == "--server")
|
||||
{
|
||||
await RunMcpServerAsync();
|
||||
return;
|
||||
}
|
||||
|
||||
var endpoint = Environment.GetEnvironmentVariable("AZURE_OPENAI_ENDPOINT") ?? throw new InvalidOperationException("AZURE_OPENAI_ENDPOINT is not set.");
|
||||
var deploymentName = Environment.GetEnvironmentVariable("AZURE_OPENAI_DEPLOYMENT_NAME") ?? "gpt-5.4-mini";
|
||||
|
||||
// Launch this same assembly as a stdio MCP server in a child process.
|
||||
var thisAssemblyPath = typeof(Program).Assembly.Location;
|
||||
await using var mcpClient = await McpClient.CreateAsync(new StdioClientTransport(new()
|
||||
{
|
||||
Name = "DatasetAnalyzer",
|
||||
Command = "dotnet",
|
||||
Arguments = [thisAssemblyPath, "--server"],
|
||||
}));
|
||||
|
||||
// Wrap each MCP tool with task-aware behavior. The wrapper inspects the server's
|
||||
// execution.taskSupport on each tool and, when it is Required, drives the task lifecycle
|
||||
// transparently within the agent's tool loop. Tools that don't require task semantics are
|
||||
// returned as-is and invoked inline.
|
||||
var taskOptions = new McpTaskOptions
|
||||
{
|
||||
DefaultTimeToLive = TimeSpan.FromMinutes(5),
|
||||
};
|
||||
var mcpTools = await mcpClient.ListAgentToolsWithTaskSupportAsync(taskOptions);
|
||||
|
||||
// WARNING: DefaultAzureCredential is convenient for development but requires careful consideration in production.
|
||||
// In production, consider using a specific credential (e.g., ManagedIdentityCredential) to avoid
|
||||
// latency issues, unintended credential probing, and potential security risks from fallback mechanisms.
|
||||
AIAgent agent = new AzureOpenAIClient(
|
||||
new Uri(endpoint),
|
||||
new DefaultAzureCredential())
|
||||
.GetChatClient(deploymentName)
|
||||
.AsAIAgent(
|
||||
instructions: "You answer data-analysis questions by invoking the available tools. Always invoke a tool when one matches the request.",
|
||||
tools: [.. mcpTools.Cast<AITool>()]);
|
||||
|
||||
const string Prompt = "Analyze the dataset named 'sales-2025-q1' and summarize the findings.";
|
||||
|
||||
Console.WriteLine("=== Transparent long-running MCP task (RunAsync) ===");
|
||||
Console.WriteLine("Asking the agent to analyze a dataset; the tool takes ~15s to complete.");
|
||||
Console.WriteLine("RunAsync blocks while the wrapper polls the task to completion.");
|
||||
Console.WriteLine();
|
||||
|
||||
var stopwatch = System.Diagnostics.Stopwatch.StartNew();
|
||||
var response = await agent.RunAsync(Prompt);
|
||||
stopwatch.Stop();
|
||||
|
||||
Console.WriteLine($"Agent response (after {stopwatch.Elapsed.TotalSeconds:F1}s):");
|
||||
Console.WriteLine(response.Text);
|
||||
|
||||
Console.WriteLine();
|
||||
Console.WriteLine("=== Transparent long-running MCP task (RunStreamingAsync) ===");
|
||||
Console.WriteLine("Same request via the streaming API. Updates only begin to arrive after the");
|
||||
Console.WriteLine("tool's task reaches the Completed state, since the model needs the tool result");
|
||||
Console.WriteLine("before it can produce its final answer.");
|
||||
Console.WriteLine();
|
||||
|
||||
stopwatch.Restart();
|
||||
await foreach (var update in agent.RunStreamingAsync(Prompt))
|
||||
{
|
||||
Console.Write(update.Text);
|
||||
}
|
||||
stopwatch.Stop();
|
||||
|
||||
Console.WriteLine();
|
||||
Console.WriteLine($"(Streaming completed after {stopwatch.Elapsed.TotalSeconds:F1}s.)");
|
||||
|
||||
// --- Server mode (launched as a child process via --server) ---------------------------------
|
||||
static async Task RunMcpServerAsync()
|
||||
{
|
||||
var builder = Host.CreateApplicationBuilder();
|
||||
|
||||
// Critical for stdio transport: any provider that writes to stdout will corrupt the
|
||||
// JSON-RPC channel. Clear all providers; the MCP SDK routes its own diagnostics
|
||||
// appropriately.
|
||||
builder.Logging.ClearProviders();
|
||||
builder.Logging.AddConsole(o => o.LogToStandardErrorThreshold = LogLevel.Trace);
|
||||
|
||||
builder.Services.AddMcpServer(o =>
|
||||
{
|
||||
o.TaskStore = new InMemoryMcpTaskStore();
|
||||
o.ServerInfo = new Implementation { Name = "DatasetAnalyzer", Version = "1.0.0" };
|
||||
})
|
||||
.WithStdioServerTransport()
|
||||
.WithTools<DatasetAnalysisTools>();
|
||||
|
||||
await builder.Build().RunAsync();
|
||||
}
|
||||
|
||||
#pragma warning disable CA1812 // Discovered by MCP SDK via [McpServerToolType] attribute
|
||||
[McpServerToolType]
|
||||
internal sealed class DatasetAnalysisTools
|
||||
#pragma warning restore CA1812
|
||||
{
|
||||
[McpServerTool(Name = "AnalyzeDataset", TaskSupport = ToolTaskSupport.Required)]
|
||||
[Description("Analyze a tabular dataset and return summary statistics. This tool simulates a long-running analytic job (~15 seconds).")]
|
||||
public static async Task<string> AnalyzeDatasetAsync(
|
||||
[Description("The dataset identifier, e.g. 'sales-2025-q1'.")] string datasetName,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
await Task.Delay(TimeSpan.FromSeconds(15), cancellationToken).ConfigureAwait(false);
|
||||
|
||||
return $"Findings for '{datasetName}': 12,403 rows; avg revenue $48,712; 3 anomalies detected in week 7; outliers concentrated in EMEA region.";
|
||||
}
|
||||
}
|
||||
+60
@@ -0,0 +1,60 @@
|
||||
# Agent with MCP long-running task (transparent polling)
|
||||
|
||||
This sample demonstrates Microsoft Agent Framework's MCP long-running task support: an agent invokes an MCP tool whose execution takes too long for a single request/response cycle, and the framework polls it to completion behind the function-calling loop. From the agent's perspective the tool simply returns its result.
|
||||
|
||||
## What this sample shows
|
||||
|
||||
- Using `McpClient.ListAgentToolsWithTaskSupportAsync(...)` (in `Microsoft.Agents.AI.Mcp`) to wrap MCP tools with task-aware behavior.
|
||||
- Configuring `McpTaskOptions.DefaultTimeToLive` to bound the server-side task.
|
||||
- Hosting a small MCP server (in this same executable, launched with `--server`) that advertises `execution.taskSupport=required` on a tool that sleeps for ~15 seconds.
|
||||
- No application-level polling, continuation tokens, or `AllowBackgroundResponses` flag are required.
|
||||
|
||||
The decorator drives the lifecycle internally:
|
||||
|
||||
1. `tools/call` augmented with task metadata (`CallToolAsTaskAsync`)
|
||||
2. `tasks/get` polled until terminal (`PollTaskUntilCompleteAsync`)
|
||||
3. `tasks/result` retrieved (`GetTaskResultAsync`) and returned to the function-calling loop
|
||||
|
||||
The sample exercises both invocation styles against the same wrapper:
|
||||
|
||||
- `agent.RunAsync(...)` blocks until the tool completes (~15 seconds in this sample) and returns the final response.
|
||||
- `agent.RunStreamingAsync(...)` returns immediately and yields `AgentResponseUpdate` chunks as the model emits them; in this scenario the model only begins streaming its answer once the wrapped tool's task reaches the `Completed` state, so the perceived "pause" before tokens arrive reflects tool execution time, not stream-channel latency.
|
||||
|
||||
# Prerequisites
|
||||
|
||||
- .NET 10 SDK or later
|
||||
- Azure OpenAI service endpoint and a chat-completions deployment
|
||||
- Azure CLI installed and authenticated (`az login`)
|
||||
|
||||
Set the following environment variables:
|
||||
|
||||
```powershell
|
||||
$env:AZURE_OPENAI_ENDPOINT="https://your-resource.openai.azure.com/"
|
||||
$env:AZURE_OPENAI_DEPLOYMENT_NAME="gpt-5.4-mini" # optional; defaults to gpt-5.4-mini
|
||||
```
|
||||
|
||||
# Running
|
||||
|
||||
```powershell
|
||||
cd Agent_MCP_LongRunningTask_Client
|
||||
dotnet run
|
||||
```
|
||||
|
||||
You should see output similar to:
|
||||
|
||||
```
|
||||
=== Transparent long-running MCP task (RunAsync) ===
|
||||
Asking the agent to analyze a dataset; the tool takes ~15s to complete.
|
||||
RunAsync blocks while the wrapper polls the task to completion.
|
||||
|
||||
Agent response (after 15.4s):
|
||||
The 'sales-2025-q1' dataset contains 12,403 rows ...
|
||||
|
||||
=== Transparent long-running MCP task (RunStreamingAsync) ===
|
||||
Same request via the streaming API. Updates only begin to arrive after the
|
||||
tool's task reaches the Completed state, since the model needs the tool result
|
||||
before it can produce its final answer.
|
||||
|
||||
The 'sales-2025-q1' dataset contains 12,403 rows ...
|
||||
(Streaming completed after 15.7s.)
|
||||
```
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFrameworks>net10.0</TargetFrameworks>
|
||||
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Azure.Identity" />
|
||||
<PackageReference Include="ModelContextProtocol" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\..\..\src\Microsoft.Agents.AI\Microsoft.Agents.AI.csproj" />
|
||||
<ProjectReference Include="..\..\..\..\src\Microsoft.Agents.AI.Foundry\Microsoft.Agents.AI.Foundry.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
+136
@@ -0,0 +1,136 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
// This sample shows how to attach per-run (refreshable) authentication headers to MCP requests.
|
||||
//
|
||||
// The agent connects to an MCP server with a custom HttpClient. A DelegatingHandler reads a token
|
||||
// for the current run from an AsyncLocal scope and stamps it on each outbound MCP request, so a
|
||||
// short-lived token (for example an OBO or cloud identity token that expires) can be refreshed on
|
||||
// every run without rebuilding the agent or the MCP connection.
|
||||
//
|
||||
// The agent backend is Microsoft Foundry via the Responses API (RAPI). The MCP server is the public
|
||||
// Microsoft Learn MCP server, which ignores the demonstration token; in production you point the
|
||||
// handler at your own protected MCP server and mint a real token per run.
|
||||
|
||||
using System.Net.Http.Headers;
|
||||
using Azure.AI.Projects;
|
||||
using Azure.Identity;
|
||||
using Microsoft.Agents.AI;
|
||||
using Microsoft.Extensions.AI;
|
||||
using ModelContextProtocol.Client;
|
||||
|
||||
var projectEndpoint = new Uri(Environment.GetEnvironmentVariable("FOUNDRY_PROJECT_ENDPOINT")
|
||||
?? throw new InvalidOperationException("FOUNDRY_PROJECT_ENDPOINT is not set."));
|
||||
var deploymentName = Environment.GetEnvironmentVariable("FOUNDRY_MODEL") ?? "gpt-5.4-mini";
|
||||
|
||||
var serverEndpoint = new Uri("https://learn.microsoft.com/api/mcp");
|
||||
|
||||
// Custom HttpClient for the MCP transport. The per-run handler attaches the bearer; the inner
|
||||
// handler disables cookies (no cross-context state), disables auto-redirect (so a redirect cannot
|
||||
// carry the bearer past the origin re-check), and checks certificate revocation.
|
||||
using var httpClient = new HttpClient(new PerRunAuthHeaderHandler(serverEndpoint)
|
||||
{
|
||||
InnerHandler = new HttpClientHandler
|
||||
{
|
||||
UseCookies = false,
|
||||
AllowAutoRedirect = false,
|
||||
CheckCertificateRevocationList = true,
|
||||
},
|
||||
});
|
||||
|
||||
Console.WriteLine($"Connecting to MCP server at {serverEndpoint} ...");
|
||||
|
||||
await using var mcpClient = await McpClient.CreateAsync(new HttpClientTransport(new()
|
||||
{
|
||||
Endpoint = serverEndpoint,
|
||||
Name = "Microsoft Learn MCP",
|
||||
TransportMode = HttpTransportMode.StreamableHttp,
|
||||
}, httpClient));
|
||||
|
||||
IList<McpClientTool> mcpTools = await mcpClient.ListToolsAsync();
|
||||
Console.WriteLine($"MCP tools available: {string.Join(", ", mcpTools.Select(t => t.Name))}");
|
||||
|
||||
// Build the agent from Microsoft Foundry using the Responses API (RAPI).
|
||||
// WARNING: DefaultAzureCredential is convenient for development but requires careful consideration in production.
|
||||
// In production, consider using a specific credential (e.g., ManagedIdentityCredential) to avoid
|
||||
// latency issues, unintended credential probing, and potential security risks from fallback mechanisms.
|
||||
AIAgent agent = new AIProjectClient(projectEndpoint, new DefaultAzureCredential())
|
||||
.AsAIAgent(
|
||||
model: deploymentName,
|
||||
instructions: "You answer Microsoft documentation questions using the available tools.",
|
||||
name: "DocsAgent",
|
||||
tools: [.. mcpTools.Cast<AITool>()]);
|
||||
|
||||
// Run the same agent twice under two different contexts. Each run gets a freshly minted token,
|
||||
// proving the auth header is per-run rather than bound when the agent or MCP connection was created.
|
||||
await RunForContextAsync(agent, "tenant-a", "How do I create an Azure storage account with az cli?");
|
||||
await RunForContextAsync(agent, "tenant-b", "What is Azure Functions?");
|
||||
|
||||
static async Task RunForContextAsync(AIAgent agent, string label, string prompt)
|
||||
{
|
||||
// Stand-in for a real per-run token (for example an OBO or cloud identity token).
|
||||
// It carries no PII and is regenerated on every run. The label is non-secret and used for logging.
|
||||
McpRunContext? previous = McpRunScope.Current;
|
||||
McpRunScope.Current = new McpRunContext(label, $"{label}.{Guid.NewGuid():N}");
|
||||
try
|
||||
{
|
||||
Console.WriteLine($"\n=== Run for '{label}' (fresh per-run token) ===");
|
||||
Console.WriteLine(await agent.RunAsync(prompt));
|
||||
}
|
||||
finally
|
||||
{
|
||||
// Restore the prior scope (stack-like) so this is safe to call from within an outer scope.
|
||||
McpRunScope.Current = previous;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Carries the context for the current run. <see cref="Label"/> is a non-secret identifier safe to
|
||||
/// log; <see cref="Token"/> is the secret that must never be logged or persisted.
|
||||
/// </summary>
|
||||
internal sealed record McpRunContext(string Label, string Token);
|
||||
|
||||
/// <summary>
|
||||
/// Flows the current <see cref="McpRunContext"/> to the MCP <see cref="DelegatingHandler"/> without
|
||||
/// threading it through every call. Set it before a run and reset it afterwards.
|
||||
/// </summary>
|
||||
internal static class McpRunScope
|
||||
{
|
||||
private static readonly AsyncLocal<McpRunContext?> s_current = new();
|
||||
|
||||
public static McpRunContext? Current
|
||||
{
|
||||
get => s_current.Value;
|
||||
set => s_current.Value = value;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Attaches the current run's bearer token to outbound MCP requests. The token is read fresh on
|
||||
/// every request, so refreshing it between runs needs no agent or connection rebuild.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Security: the bearer is attached only over HTTPS and only when the request targets the configured
|
||||
/// MCP server origin, which prevents the credential from leaking over plaintext or to a redirect
|
||||
/// target on another origin. Only the non-secret label is logged, never the token.
|
||||
/// </remarks>
|
||||
internal sealed class PerRunAuthHeaderHandler(Uri serverEndpoint) : DelegatingHandler
|
||||
{
|
||||
private readonly string _serverOrigin = serverEndpoint.GetLeftPart(UriPartial.Authority);
|
||||
|
||||
protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
|
||||
{
|
||||
McpRunContext? context = McpRunScope.Current;
|
||||
Uri? requestUri = request.RequestUri;
|
||||
|
||||
if (context is not null
|
||||
&& requestUri is not null
|
||||
&& requestUri.Scheme == Uri.UriSchemeHttps
|
||||
&& string.Equals(requestUri.GetLeftPart(UriPartial.Authority), this._serverOrigin, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", context.Token);
|
||||
Console.WriteLine($"[mcp-auth] attached bearer for '{context.Label}' -> {request.Method} {requestUri.AbsolutePath}");
|
||||
}
|
||||
|
||||
return await base.SendAsync(request, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
# Per-Run MCP Authentication Headers
|
||||
|
||||
This sample shows how to attach per-run (refreshable) authentication headers to Model Context
|
||||
Protocol (MCP) requests using existing Agent Framework primitives. It addresses scenarios where the
|
||||
header value changes from one run to the next, for example a short-lived On-Behalf-Of (OBO) or cloud
|
||||
identity token that expires and must be refreshed.
|
||||
|
||||
The agent backend is Microsoft Foundry accessed through the Responses API (RAPI). The MCP server is
|
||||
the public Microsoft Learn MCP server.
|
||||
|
||||
## What this sample demonstrates
|
||||
|
||||
- A custom `HttpClient` on the MCP transport whose `DelegatingHandler` stamps an `Authorization`
|
||||
header on every outbound MCP request.
|
||||
- An `AsyncLocal` scope (`McpRunScope`) that carries the current run's context to the handler, set
|
||||
immediately before each run and cleared in a `finally` block.
|
||||
- Running the same agent twice under two different contexts, each with a freshly minted token, so the
|
||||
header is per-run rather than fixed when the agent or the MCP connection was created.
|
||||
|
||||
Because the handler reads the token fresh on every request, an expiring token is refreshed simply by
|
||||
placing a new value in scope before the next run. No agent or connection rebuild is required.
|
||||
|
||||
## How it works
|
||||
|
||||
```text
|
||||
RunForContextAsync sets McpRunScope.Current
|
||||
-> agent.RunAsync invokes an MCP tool
|
||||
-> PerRunAuthHeaderHandler reads McpRunScope.Current
|
||||
-> stamps Authorization: Bearer <token> on the MCP request
|
||||
RunForContextAsync clears McpRunScope.Current in finally
|
||||
```
|
||||
|
||||
The public Microsoft Learn MCP server is anonymous and ignores the demonstration token. In production
|
||||
you point the handler at your own protected MCP server and mint a real token per run.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- .NET 10 SDK or later
|
||||
- A Microsoft Foundry project endpoint and a model deployment
|
||||
- An authenticated Azure identity (for example, sign in with `az login`)
|
||||
|
||||
Set the following environment variables:
|
||||
|
||||
```powershell
|
||||
$env:FOUNDRY_PROJECT_ENDPOINT="https://your-foundry-service.services.ai.azure.com/api/projects/your-foundry-project"
|
||||
$env:FOUNDRY_MODEL="gpt-5.4-mini"
|
||||
```
|
||||
|
||||
## Run the sample
|
||||
|
||||
```powershell
|
||||
dotnet run
|
||||
```
|
||||
|
||||
## Security considerations
|
||||
|
||||
This sample is written to demonstrate the pattern safely. When you adapt it, keep these in place:
|
||||
|
||||
- **Never log the token.** Only the non-secret label is printed. Avoid printing the token even in a
|
||||
masked form.
|
||||
- **Attach the header over HTTPS only.** The handler skips the header when the request is not HTTPS,
|
||||
so a credential is never sent over plaintext.
|
||||
- **Scope the header to the MCP server origin.** The handler attaches the header only when the
|
||||
request targets the configured server origin (scheme, host, and port). Auto-redirect is also
|
||||
disabled (`AllowAutoRedirect = false`) so a redirect cannot carry the token to another origin
|
||||
below the handler before the origin check runs.
|
||||
- **Reset the scope after each run.** `McpRunScope.Current` is restored to its prior value in a
|
||||
`finally` block so a token does not bleed into later, unrelated work and nesting stays safe.
|
||||
- **Disable cookies on the shared handler.** `UseCookies = false` avoids cross-context state on a
|
||||
shared client, and `CheckCertificateRevocationList = true` validates the server certificate.
|
||||
- **Use non-identifying labels and tokens.** The labels and tokens here carry no personal data and are
|
||||
regenerated per run.
|
||||
- **Do not persist secrets in serialized session state.** Agent session state is serializable, so keep
|
||||
raw tokens in memory or mint them per run rather than storing them there.
|
||||
|
||||
## Production notes
|
||||
|
||||
- Replace the demonstration token with a real per-request exchange inside the handler, for example an
|
||||
Azure `TokenCredential`, MSAL OBO flow, or a cloud identity token. Performing the exchange per
|
||||
request lets expiry self-heal because each request obtains a current token.
|
||||
- The `AsyncLocal` scope isolates concurrent runs from each other, so parallel runs with different
|
||||
tokens do not interfere.
|
||||
- As an alternative carrier, the token can be read from `AgentSession` state by an `AIContextProvider`
|
||||
that copies it into the scope at the start of each invocation. Remember the serialized-state warning
|
||||
above and avoid persisting the raw secret.
|
||||
- For MCP servers that implement standard OAuth, `HttpClientTransportOptions.OAuth` already handles the
|
||||
authorization and refresh flow, so a custom handler is unnecessary.
|
||||
- This sample attaches the same header for every tool call in a run. Selecting different headers based
|
||||
on the specific tool or its arguments is intentionally out of scope here.
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFrameworks>net10.0</TargetFrameworks>
|
||||
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Azure.AI.OpenAI" />
|
||||
<PackageReference Include="Azure.Identity" />
|
||||
<PackageReference Include="Microsoft.Extensions.AI.OpenAI" />
|
||||
<PackageReference Include="ModelContextProtocol" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\..\..\src\Microsoft.Agents.AI.OpenAI\Microsoft.Agents.AI.OpenAI.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -0,0 +1,36 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
// This sample shows how to create and use a simple AI agent with tools from an MCP Server.
|
||||
|
||||
using Azure.AI.OpenAI;
|
||||
using Azure.Identity;
|
||||
using Microsoft.Agents.AI;
|
||||
using Microsoft.Extensions.AI;
|
||||
using ModelContextProtocol.Client;
|
||||
using OpenAI.Chat;
|
||||
|
||||
var endpoint = Environment.GetEnvironmentVariable("AZURE_OPENAI_ENDPOINT") ?? throw new InvalidOperationException("AZURE_OPENAI_ENDPOINT is not set.");
|
||||
var deploymentName = Environment.GetEnvironmentVariable("AZURE_OPENAI_DEPLOYMENT_NAME") ?? "gpt-5.4-mini";
|
||||
|
||||
// Create an MCPClient for the GitHub server
|
||||
await using var mcpClient = await McpClient.CreateAsync(new StdioClientTransport(new()
|
||||
{
|
||||
Name = "MCPServer",
|
||||
Command = "npx",
|
||||
Arguments = ["-y", "--verbose", "@modelcontextprotocol/server-github"],
|
||||
}));
|
||||
|
||||
// Retrieve the list of tools available on the GitHub server
|
||||
var mcpTools = await mcpClient.ListToolsAsync().ConfigureAwait(false);
|
||||
|
||||
// WARNING: DefaultAzureCredential is convenient for development but requires careful consideration in production.
|
||||
// In production, consider using a specific credential (e.g., ManagedIdentityCredential) to avoid
|
||||
// latency issues, unintended credential probing, and potential security risks from fallback mechanisms.
|
||||
AIAgent agent = new AzureOpenAIClient(
|
||||
new Uri(endpoint),
|
||||
new DefaultAzureCredential())
|
||||
.GetChatClient(deploymentName)
|
||||
.AsAIAgent(instructions: "You answer questions related to GitHub repositories only.", tools: [.. mcpTools.Cast<AITool>()]);
|
||||
|
||||
// Invoke the agent and output the text result.
|
||||
Console.WriteLine(await agent.RunAsync("Summarize the last four commits to the microsoft/semantic-kernel repository?"));
|
||||
@@ -0,0 +1,31 @@
|
||||
# Model Context Protocol Sample
|
||||
|
||||
This example demonstrates how to use tools from a Model Context Protocol server with Agent Framework.
|
||||
|
||||
MCP is an open protocol that standardizes how applications provide context to LLMs.
|
||||
|
||||
For information on Model Context Protocol (MCP) please refer to the [documentation](https://modelcontextprotocol.io/introduction).
|
||||
|
||||
The sample shows:
|
||||
|
||||
1. How to connect to an MCP Server
|
||||
1. Retrieve the list of tools the MCP Server makes available
|
||||
1. Convert the MCP tools to `AIFunction`'s so they can be added to an agent
|
||||
1. Invoke the tools from an agent using function calling
|
||||
|
||||
## Configuring Environment Variables
|
||||
|
||||
Set the following environment variables:
|
||||
|
||||
```powershell
|
||||
$env:AZURE_OPENAI_ENDPOINT="https://your-resource.openai.azure.com/" # Replace with your Azure OpenAI resource endpoint
|
||||
$env:AZURE_OPENAI_DEPLOYMENT_NAME="gpt-5.4-mini" # Optional, defaults to gpt-5.4-mini
|
||||
```
|
||||
|
||||
## Setup and Running
|
||||
|
||||
Run the Agent_MCP_Server sample
|
||||
|
||||
```bash
|
||||
dotnet run
|
||||
```
|
||||
+24
@@ -0,0 +1,24 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFrameworks>net10.0</TargetFrameworks>
|
||||
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Azure.AI.OpenAI" />
|
||||
<PackageReference Include="Azure.Identity" />
|
||||
<PackageReference Include="Microsoft.Extensions.AI.OpenAI" />
|
||||
<PackageReference Include="Microsoft.Extensions.Logging" />
|
||||
<PackageReference Include="Microsoft.Extensions.Logging.Console" />
|
||||
<PackageReference Include="ModelContextProtocol" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\..\..\src\Microsoft.Agents.AI.OpenAI\Microsoft.Agents.AI.OpenAI.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -0,0 +1,145 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
// This sample shows how to create and use a simple AI agent with tools from an MCP Server that requires authentication.
|
||||
|
||||
using System.Diagnostics;
|
||||
using System.Net;
|
||||
using System.Text;
|
||||
using System.Web;
|
||||
using Azure.AI.OpenAI;
|
||||
using Azure.Identity;
|
||||
using Microsoft.Agents.AI;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using ModelContextProtocol.Client;
|
||||
using OpenAI.Chat;
|
||||
|
||||
var endpoint = Environment.GetEnvironmentVariable("AZURE_OPENAI_ENDPOINT") ?? throw new InvalidOperationException("AZURE_OPENAI_ENDPOINT is not set.");
|
||||
var deploymentName = Environment.GetEnvironmentVariable("AZURE_OPENAI_DEPLOYMENT_NAME") ?? "gpt-5.4-mini";
|
||||
|
||||
// We can customize a shared HttpClient with a custom handler if desired
|
||||
using var sharedHandler = new SocketsHttpHandler
|
||||
{
|
||||
PooledConnectionLifetime = TimeSpan.FromMinutes(2),
|
||||
PooledConnectionIdleTimeout = TimeSpan.FromMinutes(1)
|
||||
};
|
||||
using var httpClient = new HttpClient(sharedHandler);
|
||||
|
||||
var consoleLoggerFactory = LoggerFactory.Create(builder => builder.AddConsole());
|
||||
|
||||
// Create SSE client transport for the MCP server
|
||||
var serverUrl = "http://localhost:7071/";
|
||||
var transport = new HttpClientTransport(new()
|
||||
{
|
||||
Endpoint = new Uri(serverUrl),
|
||||
Name = "Secure Weather Client",
|
||||
OAuth = new()
|
||||
{
|
||||
DynamicClientRegistration = new()
|
||||
{
|
||||
ClientName = "ProtectedMcpClient",
|
||||
},
|
||||
RedirectUri = new Uri("http://localhost:1179/callback"),
|
||||
AuthorizationRedirectDelegate = HandleAuthorizationUrlAsync,
|
||||
}
|
||||
}, httpClient, consoleLoggerFactory);
|
||||
|
||||
// Create an MCPClient for the protected MCP server
|
||||
await using var mcpClient = await McpClient.CreateAsync(transport, loggerFactory: consoleLoggerFactory);
|
||||
|
||||
// Retrieve the list of tools available on the GitHub server
|
||||
var mcpTools = await mcpClient.ListToolsAsync().ConfigureAwait(false);
|
||||
|
||||
// WARNING: DefaultAzureCredential is convenient for development but requires careful consideration in production.
|
||||
// In production, consider using a specific credential (e.g., ManagedIdentityCredential) to avoid
|
||||
// latency issues, unintended credential probing, and potential security risks from fallback mechanisms.
|
||||
AIAgent agent = new AzureOpenAIClient(
|
||||
new Uri(endpoint),
|
||||
new DefaultAzureCredential())
|
||||
.GetChatClient(deploymentName)
|
||||
.AsAIAgent(instructions: "You answer questions related to the weather.", tools: [.. mcpTools]);
|
||||
|
||||
// Invoke the agent and output the text result.
|
||||
Console.WriteLine(await agent.RunAsync("Get current weather alerts for New York?"));
|
||||
|
||||
// Handles the OAuth authorization URL by starting a local HTTP server and opening a browser.
|
||||
// This implementation demonstrates how SDK consumers can provide their own authorization flow.
|
||||
static async Task<string?> HandleAuthorizationUrlAsync(Uri authorizationUrl, Uri redirectUri, CancellationToken cancellationToken)
|
||||
{
|
||||
Console.WriteLine("Starting OAuth authorization flow...");
|
||||
Console.WriteLine($"Opening browser to: {authorizationUrl}");
|
||||
|
||||
var listenerPrefix = redirectUri.GetLeftPart(UriPartial.Authority);
|
||||
if (!listenerPrefix.EndsWith("/", StringComparison.InvariantCultureIgnoreCase))
|
||||
{
|
||||
listenerPrefix += "/";
|
||||
}
|
||||
|
||||
using var listener = new HttpListener();
|
||||
listener.Prefixes.Add(listenerPrefix);
|
||||
|
||||
try
|
||||
{
|
||||
listener.Start();
|
||||
Console.WriteLine($"Listening for OAuth callback on: {listenerPrefix}");
|
||||
|
||||
OpenBrowser(authorizationUrl);
|
||||
|
||||
var context = await listener.GetContextAsync();
|
||||
var query = HttpUtility.ParseQueryString(context.Request.Url?.Query ?? string.Empty);
|
||||
var code = query["code"];
|
||||
var error = query["error"];
|
||||
|
||||
const string ResponseHtml = "<html><body><h1>Authentication complete</h1><p>You can close this window now.</p></body></html>";
|
||||
byte[] buffer = Encoding.UTF8.GetBytes(ResponseHtml);
|
||||
context.Response.ContentLength64 = buffer.Length;
|
||||
context.Response.ContentType = "text/html";
|
||||
context.Response.OutputStream.Write(buffer, 0, buffer.Length);
|
||||
context.Response.Close();
|
||||
|
||||
if (!string.IsNullOrEmpty(error))
|
||||
{
|
||||
Console.WriteLine($"Auth error: {error}");
|
||||
return null;
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(code))
|
||||
{
|
||||
Console.WriteLine("No authorization code received");
|
||||
return null;
|
||||
}
|
||||
|
||||
Console.WriteLine("Authorization code received successfully.");
|
||||
return code;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($"Error getting auth code: {ex.Message}");
|
||||
return null;
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (listener.IsListening)
|
||||
{
|
||||
listener.Stop();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Opens the specified URL in the default browser.
|
||||
static void OpenBrowser(Uri url)
|
||||
{
|
||||
try
|
||||
{
|
||||
var psi = new ProcessStartInfo
|
||||
{
|
||||
FileName = url.ToString(),
|
||||
UseShellExecute = true
|
||||
};
|
||||
Process.Start(psi);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($"Error opening browser. {ex.Message}");
|
||||
Console.WriteLine($"Please manually open this URL: {url}");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,125 @@
|
||||
# Model Context Protocol Sample
|
||||
|
||||
This example demonstrates how to use tools from a protected Model Context Protocol server with Agent Framework.
|
||||
|
||||
MCP is an open protocol that standardizes how applications provide context to LLMs.
|
||||
|
||||
For information on Model Context Protocol (MCP) please refer to the [documentation](https://modelcontextprotocol.io/introduction).
|
||||
|
||||
The sample shows:
|
||||
|
||||
1. How to connect to a protected MCP Server using OAuth 2.0 authentication
|
||||
1. How to implement a custom OAuth authorization flow with browser-based authentication
|
||||
1. Retrieve the list of tools the MCP Server makes available
|
||||
1. Convert the MCP tools to `AIFunction`'s so they can be added to an agent
|
||||
1. Invoke the tools from an agent using function calling
|
||||
|
||||
## Installing Prerequisites
|
||||
|
||||
- A self-signed certificate to enable HTTPS use in development, see [dotnet dev-certs](https://learn.microsoft.com/en-us/dotnet/core/tools/dotnet-dev-certs)
|
||||
- .NET 10.0 or later
|
||||
- A running TestOAuthServer (for OAuth authentication), see [Start the Test OAuth Server](https://github.com/modelcontextprotocol/csharp-sdk/tree/main/samples/ProtectedMcpClient#step-1-start-the-test-oauth-server)
|
||||
- A running ProtectedMCPServer (for MCP services), see [Start the Protected MCP Server](https://github.com/modelcontextprotocol/csharp-sdk/tree/main/samples/ProtectedMcpClient#step-2-start-the-protected-mcp-server)
|
||||
|
||||
## Configuring Environment Variables
|
||||
|
||||
Set the following environment variables:
|
||||
|
||||
```powershell
|
||||
$env:AZURE_OPENAI_ENDPOINT="https://your-resource.openai.azure.com/" # Replace with your Azure OpenAI resource endpoint
|
||||
$env:AZURE_OPENAI_DEPLOYMENT_NAME="gpt-5.4-mini" # Optional, defaults to gpt-5.4-mini
|
||||
```
|
||||
|
||||
## Setup and Running
|
||||
|
||||
### Step 1: Start the Test OAuth Server
|
||||
|
||||
First, you need to start the TestOAuthServer which provides OAuth authentication:
|
||||
|
||||
```bash
|
||||
cd <MCP CSHARP-SDK>\tests\ModelContextProtocol.TestOAuthServer
|
||||
dotnet run --framework net10.0
|
||||
```
|
||||
|
||||
The OAuth server will start at `https://localhost:7029`
|
||||
|
||||
### Step 2: Start the Protected MCP Server
|
||||
|
||||
Next, start the ProtectedMCPServer which provides the weather tools:
|
||||
|
||||
```bash
|
||||
cd <MCP CSHARP-SDK>\samples\ProtectedMCPServer
|
||||
dotnet run
|
||||
```
|
||||
|
||||
The protected server will start at `http://localhost:7071`
|
||||
|
||||
### Step 3: Run the Agent_MCP_Server_Auth sample
|
||||
|
||||
Finally, run this client:
|
||||
|
||||
```bash
|
||||
dotnet run
|
||||
```
|
||||
|
||||
## What Happens
|
||||
|
||||
1. The client attempts to connect to the protected MCP server at `http://localhost:7071`
|
||||
2. The server responds with OAuth metadata indicating authentication is required
|
||||
3. The client initiates OAuth 2.0 authorization code flow:
|
||||
- Opens a browser to the authorization URL at the OAuth server
|
||||
- Starts a local HTTP listener on `http://localhost:1179/callback` to receive the authorization code
|
||||
- Exchanges the authorization code for an access token
|
||||
4. The client uses the access token to authenticate with the MCP server
|
||||
5. The client lists available tools and calls the `GetAlerts` tool for New York state
|
||||
|
||||
The following diagram outlines an example OAuth flow:
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant Client as Client
|
||||
participant Server as MCP Server (Resource Server)
|
||||
participant AuthServer as Authorization Server
|
||||
|
||||
Client->>Server: MCP request without access token
|
||||
Server-->>Client: HTTP 401 Unauthorized with WWW-Authenticate header
|
||||
Note over Client: Analyze and delegate tasks
|
||||
Client->>Server: GET /.well-known/oauth-protected-resource
|
||||
Server-->>Client: Resource metadata with authorization server URL
|
||||
Note over Client: Validate RS metadata, build AS metadata URL
|
||||
Client->>AuthServer: GET /.well-known/oauth-authorization-server
|
||||
AuthServer-->>Client: Authorization server metadata
|
||||
Note over Client,AuthServer: OAuth 2.0 authorization flow happens here
|
||||
Client->>AuthServer: Token request
|
||||
AuthServer-->>Client: Access token
|
||||
Client->>Server: MCP request with access token
|
||||
Server-->>Client: MCP response
|
||||
Note over Client,Server: MCP communication continues with valid token
|
||||
```
|
||||
|
||||
## OAuth Configuration
|
||||
|
||||
The client is configured with:
|
||||
- **Client ID**: `demo-client`
|
||||
- **Client Secret**: `demo-secret`
|
||||
- **Redirect URI**: `http://localhost:1179/callback`
|
||||
- **OAuth Server**: `https://localhost:7029`
|
||||
- **Protected Resource**: `http://localhost:7071`
|
||||
|
||||
## Available Tools
|
||||
|
||||
Once authenticated, the client can access weather tools including:
|
||||
- **GetAlerts**: Get weather alerts for a US state
|
||||
- **GetForecast**: Get weather forecast for a location (latitude/longitude)
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
- Ensure the ASP.NET Core dev certificate is trusted.
|
||||
```
|
||||
dotnet dev-certs https --clean
|
||||
dotnet dev-certs https --trust
|
||||
```
|
||||
- Ensure all three services are running in the correct order
|
||||
- Check that ports 7029, 7071, and 1179 are available
|
||||
- If the browser doesn't open automatically, copy the authorization URL from the console and open it manually
|
||||
- Make sure to allow the OAuth server's self-signed certificate in your browser
|
||||
+20
@@ -0,0 +1,20 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFrameworks>net10.0</TargetFrameworks>
|
||||
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Azure.AI.Projects" />
|
||||
<PackageReference Include="Azure.Identity" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\..\..\src\Microsoft.Agents.AI.Foundry\Microsoft.Agents.AI.Foundry.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -0,0 +1,113 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
// This sample shows how to create and use a simple AI agent with Microsoft Foundry Agents as the backend, that uses a Hosted MCP Tool.
|
||||
// In this case the Microsoft Foundry Agents service will invoke any MCP tools as required. MCP tools are not invoked by the Agent Framework.
|
||||
// The sample first shows how to use MCP tools with auto approval, and then how to set up a tool that requires approval before it can be invoked and how to approve such a tool.
|
||||
|
||||
using Azure.AI.Projects;
|
||||
using Azure.AI.Projects.Agents;
|
||||
using Azure.Identity;
|
||||
using Microsoft.Agents.AI;
|
||||
using Microsoft.Extensions.AI;
|
||||
using OpenAI.Responses;
|
||||
|
||||
var endpoint = Environment.GetEnvironmentVariable("FOUNDRY_PROJECT_ENDPOINT") ?? throw new InvalidOperationException("FOUNDRY_PROJECT_ENDPOINT is not set.");
|
||||
var model = Environment.GetEnvironmentVariable("FOUNDRY_MODEL") ?? "gpt-5.4-mini";
|
||||
|
||||
// Get a client to create/retrieve server side agents with.
|
||||
// WARNING: DefaultAzureCredential is convenient for development but requires careful consideration in production.
|
||||
// In production, consider using a specific credential (e.g., ManagedIdentityCredential) to avoid
|
||||
// latency issues, unintended credential probing, and potential security risks from fallback mechanisms.
|
||||
var aiProjectClient = new AIProjectClient(new Uri(endpoint), new DefaultAzureCredential());
|
||||
|
||||
// **** MCP Tool with Auto Approval ****
|
||||
// *************************************
|
||||
|
||||
// Create an MCP tool definition that the agent can use.
|
||||
// In this case we allow the tool to always be called without approval.
|
||||
var mcpTool = ResponseTool.CreateMcpTool(
|
||||
serverLabel: "microsoft_learn",
|
||||
serverUri: new Uri("https://learn.microsoft.com/api/mcp"),
|
||||
toolCallApprovalPolicy: new McpToolCallApprovalPolicy(GlobalMcpToolCallApprovalPolicy.NeverRequireApproval));
|
||||
|
||||
// Optional: authenticate the MCP server through a Foundry project connection.
|
||||
// The connection stores credentials, so the platform injects them at request time and no inline token is sent.
|
||||
// The public Microsoft Learn MCP server above needs no authentication, so this is shown for illustration only.
|
||||
// Use the FoundryAITool.CreateMcpTool overload that takes a projectConnectionId:
|
||||
// AITool tool = FoundryAITool.CreateMcpTool(
|
||||
// serverLabel: "github",
|
||||
// serverUri: new Uri("https://api.githubcopilot.com/mcp"),
|
||||
// projectConnectionId: "my-foundry-connection",
|
||||
// toolCallApprovalPolicy: new McpToolCallApprovalPolicy(GlobalMcpToolCallApprovalPolicy.AlwaysRequireApproval));
|
||||
|
||||
// Create a server side agent with the mcp tool, and expose it as an AIAgent.
|
||||
ProjectsAgentVersion agentVersion = await aiProjectClient.AgentAdministrationClient.CreateAgentVersionAsync(
|
||||
"MicrosoftLearnAgent",
|
||||
new ProjectsAgentVersionCreationOptions(
|
||||
new DeclarativeAgentDefinition(model: model)
|
||||
{
|
||||
Instructions = "You answer questions by searching the Microsoft Learn content only.",
|
||||
Tools = { mcpTool }
|
||||
}));
|
||||
|
||||
AIAgent agent = aiProjectClient.AsAIAgent(agentVersion);
|
||||
|
||||
// You can then invoke the agent like any other AIAgent.
|
||||
AgentSession session = await agent.CreateSessionAsync();
|
||||
Console.WriteLine(await agent.RunAsync("Please summarize the Azure AI Agent documentation related to MCP Tool calling?", session));
|
||||
|
||||
// Cleanup for sample purposes.
|
||||
aiProjectClient.AgentAdministrationClient.DeleteAgent(agent.Name);
|
||||
|
||||
// **** MCP Tool with Approval Required ****
|
||||
// *****************************************
|
||||
|
||||
// Create an MCP tool definition that the agent can use.
|
||||
// In this case we require approval before the tool can be called.
|
||||
var mcpToolWithApproval = ResponseTool.CreateMcpTool(
|
||||
serverLabel: "microsoft_learn",
|
||||
serverUri: new Uri("https://learn.microsoft.com/api/mcp"),
|
||||
allowedTools: new McpToolFilter() { ToolNames = { "microsoft_docs_search" } },
|
||||
toolCallApprovalPolicy: new McpToolCallApprovalPolicy(GlobalMcpToolCallApprovalPolicy.AlwaysRequireApproval));
|
||||
|
||||
// Create an agent with the MCP tool that requires approval.
|
||||
ProjectsAgentVersion agentVersionWithApproval = await aiProjectClient.AgentAdministrationClient.CreateAgentVersionAsync(
|
||||
"MicrosoftLearnAgentWithApproval",
|
||||
new ProjectsAgentVersionCreationOptions(
|
||||
new DeclarativeAgentDefinition(model: model)
|
||||
{
|
||||
Instructions = "You answer questions by searching the Microsoft Learn content only.",
|
||||
Tools = { mcpToolWithApproval }
|
||||
}));
|
||||
|
||||
AIAgent agentWithRequiredApproval = aiProjectClient.AsAIAgent(agentVersionWithApproval);
|
||||
|
||||
// You can then invoke the agent like any other AIAgent.
|
||||
// For simplicity, we are assuming here that only mcp tool approvals are pending.
|
||||
AgentSession sessionWithRequiredApproval = await agentWithRequiredApproval.CreateSessionAsync();
|
||||
AgentResponse response = await agentWithRequiredApproval.RunAsync("Please summarize the Azure AI Agent documentation related to MCP Tool calling?", sessionWithRequiredApproval);
|
||||
List<ToolApprovalRequestContent> approvalRequests = response.Messages.SelectMany(m => m.Contents).OfType<ToolApprovalRequestContent>().ToList();
|
||||
|
||||
while (approvalRequests.Count > 0)
|
||||
{
|
||||
// Ask the user to approve each MCP call request.
|
||||
List<ChatMessage> userInputResponses = approvalRequests
|
||||
.ConvertAll(approvalRequest =>
|
||||
{
|
||||
McpServerToolCallContent mcpToolCall = (McpServerToolCallContent)approvalRequest.ToolCall!;
|
||||
Console.WriteLine($"""
|
||||
The agent would like to invoke the following MCP Tool, please reply Y to approve.
|
||||
ServerName: {mcpToolCall.ServerName}
|
||||
Name: {mcpToolCall.Name}
|
||||
Arguments: {string.Join(", ", mcpToolCall.Arguments?.Select(x => $"{x.Key}: {x.Value}") ?? [])}
|
||||
""");
|
||||
return new ChatMessage(ChatRole.User, [approvalRequest.CreateResponse(Console.ReadLine()?.Equals("Y", StringComparison.OrdinalIgnoreCase) ?? false)]);
|
||||
});
|
||||
|
||||
// Pass the user input responses back to the agent for further processing.
|
||||
response = await agentWithRequiredApproval.RunAsync(userInputResponses, sessionWithRequiredApproval);
|
||||
|
||||
approvalRequests = response.Messages.SelectMany(m => m.Contents).OfType<ToolApprovalRequestContent>().ToList();
|
||||
}
|
||||
|
||||
Console.WriteLine($"\nAgent: {response}");
|
||||
@@ -0,0 +1,37 @@
|
||||
# Prerequisites
|
||||
|
||||
Before you begin, ensure you have the following prerequisites:
|
||||
|
||||
- .NET 10 SDK or later
|
||||
- Microsoft Foundry service endpoint and deployment configured
|
||||
- Azure CLI installed and authenticated (for Azure credential authentication)
|
||||
|
||||
**Note**: This demo uses Azure CLI credentials for authentication. Make sure you're logged in with `az login` and have access to the Microsoft Foundry resource. For more information, see the [Azure CLI documentation](https://learn.microsoft.com/cli/azure/authenticate-azure-cli-interactively).
|
||||
|
||||
Set the following environment variables:
|
||||
|
||||
```powershell
|
||||
$env:FOUNDRY_PROJECT_ENDPOINT="https://your-foundry-service.services.ai.azure.com/api/projects/your-foundry-project" # Replace with your Microsoft Foundry resource endpoint
|
||||
$env:FOUNDRY_MODEL="gpt-5.4-mini" # Optional, defaults to gpt-5.4-mini
|
||||
```
|
||||
|
||||
## Authenticating a hosted MCP server with a Foundry project connection
|
||||
|
||||
A hosted MCP server can authenticate through a Foundry **project connection** instead of an inline
|
||||
authorization token or headers. The connection stores the credentials and the platform injects them
|
||||
at request time. This mirrors the Python `FoundryChatClient.get_mcp_tool(..., project_connection_id=...)`.
|
||||
|
||||
Use the `FoundryAITool.CreateMcpTool` overload that takes a `projectConnectionId`:
|
||||
|
||||
```csharp
|
||||
using Microsoft.Agents.AI.Foundry;
|
||||
using OpenAI.Responses;
|
||||
|
||||
AITool tool = FoundryAITool.CreateMcpTool(
|
||||
serverLabel: "github",
|
||||
serverUri: new Uri("https://api.githubcopilot.com/mcp"),
|
||||
projectConnectionId: "my-foundry-connection",
|
||||
toolCallApprovalPolicy: new McpToolCallApprovalPolicy(GlobalMcpToolCallApprovalPolicy.AlwaysRequireApproval));
|
||||
```
|
||||
|
||||
The resulting tool sends `project_connection_id` on the MCP tool to Foundry.
|
||||
@@ -0,0 +1,67 @@
|
||||
# Getting started with Model Content Protocol
|
||||
|
||||
The getting started with Model Content Protocol samples demonstrate how to use MCP Server tools from an agent.
|
||||
|
||||
## Getting started with agents prerequisites
|
||||
|
||||
Before you begin, ensure you have the following prerequisites:
|
||||
|
||||
- .NET 10.0 SDK or later
|
||||
- Azure OpenAI service endpoint and deployment configured
|
||||
- Azure CLI installed and authenticated (for Azure credential authentication)
|
||||
- User has the `Cognitive Services OpenAI Contributor` role for the Azure OpenAI resource.
|
||||
|
||||
**Note**: These samples use Azure OpenAI models. For more information, see [how to deploy Azure OpenAI models with Microsoft Foundry](https://learn.microsoft.com/en-us/azure/ai-foundry/how-to/deploy-models-openai).
|
||||
|
||||
**Note**: These samples use Azure CLI credentials for authentication. Make sure you're logged in with `az login` and have access to the Azure OpenAI resource and have the `Cognitive Services OpenAI Contributor` role. For more information, see the [Azure CLI documentation](https://learn.microsoft.com/cli/azure/authenticate-azure-cli-interactively).
|
||||
|
||||
## Samples
|
||||
|
||||
|Sample|Description|
|
||||
|---|---|
|
||||
|[Agent with MCP server tools](./Agent_MCP_Server/)|This sample demonstrates how to use MCP server tools with a simple agent|
|
||||
|[Agent with MCP server tools and authorization](./Agent_MCP_Server_Auth/)|This sample demonstrates how to use MCP Server tools from a protected MCP server with a simple agent|
|
||||
|[Agent with per-run MCP authentication headers](./Agent_MCP_PerRun_AuthHeaders/)|This sample demonstrates how to attach per-run, refreshable authentication headers to MCP requests using a custom HttpClient handler and an AsyncLocal scope. Uses Microsoft Foundry (`FOUNDRY_PROJECT_ENDPOINT` / `FOUNDRY_MODEL`) rather than the Azure OpenAI variables in the prerequisites above.|
|
||||
|[Responses Agent with Hosted MCP tool](./ResponseAgent_Hosted_MCP/)|This sample demonstrates how to use the Hosted MCP tool with the Responses Service, where the service invokes any MCP tools directly|
|
||||
|[Agent with long-running MCP task (transparent polling)](./Agent_MCP_LongRunningTask_Client/)|This sample demonstrates how an agent transparently drives a long-running MCP task (SEP-2663) to completion. The wrapper polls the task internally on both `RunAsync` and `RunStreamingAsync` invocations.|
|
||||
|
||||
## Running the samples from the console
|
||||
|
||||
To run the samples, navigate to the desired sample directory, e.g.
|
||||
|
||||
```powershell
|
||||
cd Agents_Step01_Running
|
||||
```
|
||||
|
||||
Set the following environment variables:
|
||||
|
||||
```powershell
|
||||
$env:AZURE_OPENAI_ENDPOINT="https://your-resource.openai.azure.com/" # Replace with your Azure OpenAI resource endpoint
|
||||
$env:AZURE_OPENAI_DEPLOYMENT_NAME="gpt-5.4-mini" # Optional, defaults to gpt-5.4-mini
|
||||
```
|
||||
|
||||
If the variables are not set, you will be prompted for the values when running the samples.
|
||||
|
||||
Execute the following command to build the sample:
|
||||
|
||||
```powershell
|
||||
dotnet build
|
||||
```
|
||||
|
||||
Execute the following command to run the sample:
|
||||
|
||||
```powershell
|
||||
dotnet run --no-build
|
||||
```
|
||||
|
||||
Or just build and run in one step:
|
||||
|
||||
```powershell
|
||||
dotnet run
|
||||
```
|
||||
|
||||
## Running the samples from Visual Studio
|
||||
|
||||
Open the solution in Visual Studio and set the desired sample project as the startup project. Then, run the project using the built-in debugger or by pressing `F5`.
|
||||
|
||||
You will be prompted for any required environment variables if they are not already set.
|
||||
@@ -0,0 +1,99 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
// This sample shows how to create and use a simple AI agent with OpenAI Responses as the backend, that uses a Hosted MCP Tool.
|
||||
// In this case the OpenAI responses service will invoke any MCP tools as required. MCP tools are not invoked by the Agent Framework.
|
||||
// The sample first shows how to use MCP tools with auto approval, and then how to set up a tool that requires approval before it can be invoked and how to approve such a tool.
|
||||
|
||||
using Azure.AI.OpenAI;
|
||||
using Azure.Identity;
|
||||
using Microsoft.Agents.AI;
|
||||
using Microsoft.Extensions.AI;
|
||||
using OpenAI.Responses;
|
||||
|
||||
var endpoint = Environment.GetEnvironmentVariable("AZURE_OPENAI_ENDPOINT") ?? throw new InvalidOperationException("AZURE_OPENAI_ENDPOINT is not set.");
|
||||
var deploymentName = Environment.GetEnvironmentVariable("AZURE_OPENAI_DEPLOYMENT_NAME") ?? "gpt-5.4-mini";
|
||||
|
||||
// **** MCP Tool with Auto Approval ****
|
||||
// *************************************
|
||||
|
||||
// Create an MCP tool definition that the agent can use.
|
||||
// In this case we allow the tool to always be called without approval.
|
||||
var mcpTool = new HostedMcpServerTool(
|
||||
serverName: "microsoft_learn",
|
||||
serverAddress: "https://learn.microsoft.com/api/mcp")
|
||||
{
|
||||
AllowedTools = ["microsoft_docs_search"],
|
||||
ApprovalMode = HostedMcpServerToolApprovalMode.NeverRequire
|
||||
};
|
||||
|
||||
// Create an agent based on Azure OpenAI Responses as the backend.
|
||||
// WARNING: DefaultAzureCredential is convenient for development but requires careful consideration in production.
|
||||
// In production, consider using a specific credential (e.g., ManagedIdentityCredential) to avoid
|
||||
// latency issues, unintended credential probing, and potential security risks from fallback mechanisms.
|
||||
AIAgent agent = new AzureOpenAIClient(
|
||||
new Uri(endpoint),
|
||||
new DefaultAzureCredential())
|
||||
.GetResponsesClient()
|
||||
.AsAIAgent(
|
||||
model: deploymentName,
|
||||
instructions: "You answer questions by searching the Microsoft Learn content only.",
|
||||
name: "MicrosoftLearnAgent",
|
||||
tools: [mcpTool]);
|
||||
|
||||
// You can then invoke the agent like any other AIAgent.
|
||||
AgentSession session = await agent.CreateSessionAsync();
|
||||
Console.WriteLine(await agent.RunAsync("Please summarize the Azure AI Agent documentation related to MCP Tool calling?", session));
|
||||
|
||||
// **** MCP Tool with Approval Required ****
|
||||
// *****************************************
|
||||
|
||||
// Create an MCP tool definition that the agent can use.
|
||||
// In this case we require approval before the tool can be called.
|
||||
var mcpToolWithApproval = new HostedMcpServerTool(
|
||||
serverName: "microsoft_learn",
|
||||
serverAddress: "https://learn.microsoft.com/api/mcp")
|
||||
{
|
||||
AllowedTools = ["microsoft_docs_search"],
|
||||
ApprovalMode = HostedMcpServerToolApprovalMode.AlwaysRequire
|
||||
};
|
||||
|
||||
// Create an agent based on Azure OpenAI Responses as the backend.
|
||||
AIAgent agentWithRequiredApproval = new AzureOpenAIClient(
|
||||
new Uri(endpoint),
|
||||
new DefaultAzureCredential())
|
||||
.GetResponsesClient()
|
||||
.AsAIAgent(
|
||||
model: deploymentName,
|
||||
instructions: "You answer questions by searching the Microsoft Learn content only.",
|
||||
name: "MicrosoftLearnAgentWithApproval",
|
||||
tools: [mcpToolWithApproval]);
|
||||
|
||||
// You can then invoke the agent like any other AIAgent.
|
||||
// For simplicity, we are assuming here that only mcp tool approvals are pending.
|
||||
AgentSession sessionWithRequiredApproval = await agentWithRequiredApproval.CreateSessionAsync();
|
||||
AgentResponse response = await agentWithRequiredApproval.RunAsync("Please summarize the Azure AI Agent documentation related to MCP Tool calling?", sessionWithRequiredApproval);
|
||||
List<ToolApprovalRequestContent> approvalRequests = response.Messages.SelectMany(m => m.Contents).OfType<ToolApprovalRequestContent>().ToList();
|
||||
|
||||
while (approvalRequests.Count > 0)
|
||||
{
|
||||
// Ask the user to approve each MCP call request.
|
||||
List<ChatMessage> userInputResponses = approvalRequests
|
||||
.ConvertAll(approvalRequest =>
|
||||
{
|
||||
McpServerToolCallContent mcpToolCall = (McpServerToolCallContent)approvalRequest.ToolCall!;
|
||||
Console.WriteLine($"""
|
||||
The agent would like to invoke the following MCP Tool, please reply Y to approve.
|
||||
ServerName: {mcpToolCall.ServerName}
|
||||
Name: {mcpToolCall.Name}
|
||||
Arguments: {string.Join(", ", mcpToolCall.Arguments?.Select(x => $"{x.Key}: {x.Value}") ?? [])}
|
||||
""");
|
||||
return new ChatMessage(ChatRole.User, [approvalRequest.CreateResponse(Console.ReadLine()?.Equals("Y", StringComparison.OrdinalIgnoreCase) ?? false)]);
|
||||
});
|
||||
|
||||
// Pass the user input responses back to the agent for further processing.
|
||||
response = await agentWithRequiredApproval.RunAsync(userInputResponses, sessionWithRequiredApproval);
|
||||
|
||||
approvalRequests = response.Messages.SelectMany(m => m.Contents).OfType<ToolApprovalRequestContent>().ToList();
|
||||
}
|
||||
|
||||
Console.WriteLine($"\nAgent: {response}");
|
||||
@@ -0,0 +1,17 @@
|
||||
# Prerequisites
|
||||
|
||||
Before you begin, ensure you have the following prerequisites:
|
||||
|
||||
- .NET 10 SDK or later
|
||||
- Azure OpenAI service endpoint and deployment configured
|
||||
- Azure CLI installed and authenticated (for Azure credential authentication)
|
||||
- User has the `Cognitive Services OpenAI Contributor` role for the Azure OpenAI resource.
|
||||
|
||||
**Note**: This demo uses Azure CLI credentials for authentication. Make sure you're logged in with `az login` and have access to the Azure OpenAI resource. For more information, see the [Azure CLI documentation](https://learn.microsoft.com/cli/azure/authenticate-azure-cli-interactively).
|
||||
|
||||
Set the following environment variables:
|
||||
|
||||
```powershell
|
||||
$env:AZURE_OPENAI_ENDPOINT="https://your-resource.openai.azure.com/" # Replace with your Azure OpenAI resource endpoint
|
||||
$env:AZURE_OPENAI_DEPLOYMENT_NAME="gpt-5.4-mini" # Optional, defaults to gpt-5.4-mini
|
||||
```
|
||||
+20
@@ -0,0 +1,20 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFrameworks>net10.0</TargetFrameworks>
|
||||
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Azure.AI.OpenAI" />
|
||||
<PackageReference Include="Azure.Identity" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\..\..\src\Microsoft.Agents.AI.OpenAI\Microsoft.Agents.AI.OpenAI.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
Reference in New Issue
Block a user