From 5b16684930f6ac484b8b1e7741a1ae2f91247b48 Mon Sep 17 00:00:00 2001
From: Roger Barreto <19890735+rogerbarreto@users.noreply.github.com>
Date: Wed, 15 Apr 2026 15:11:05 +0100
Subject: [PATCH] Add Hosted-McpTools sample with dual MCP pattern
Demonstrates two MCP integration layers in a single hosted agent:
- Client-side MCP: McpClient connects to Microsoft Learn, agent handles
tool invocations locally (docs_search, code_sample_search, docs_fetch)
- Server-side MCP: HostedMcpServerTool delegates tool discovery and
invocation to the LLM provider (Responses API), no local connection
Includes DevTemporaryTokenCredential for Docker local debugging,
Dockerfile.contributor for ProjectReference builds, and the openai/v1
route mapping for AIProjectClient compatibility in Development mode.
---
dotnet/agent-framework-dotnet.slnx | 3 +
.../HostedAgentsV2/Hosted-McpTools/.env.local | 4 +
.../HostedAgentsV2/Hosted-McpTools/Dockerfile | 17 +++
.../Hosted-McpTools/Dockerfile.contributor | 18 +++
.../Hosted-McpTools/HostedMcpTools.csproj | 31 +++++
.../HostedAgentsV2/Hosted-McpTools/Program.cs | 130 ++++++++++++++++++
.../Properties/launchSettings.json | 11 ++
.../HostedAgentsV2/Hosted-McpTools/README.md | 86 ++++++++++++
.../Hosted-McpTools/agent.manifest.yaml | 30 ++++
.../HostedAgentsV2/Hosted-McpTools/agent.yaml | 9 ++
10 files changed, 339 insertions(+)
create mode 100644 dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-McpTools/.env.local
create mode 100644 dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-McpTools/Dockerfile
create mode 100644 dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-McpTools/Dockerfile.contributor
create mode 100644 dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-McpTools/HostedMcpTools.csproj
create mode 100644 dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-McpTools/Program.cs
create mode 100644 dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-McpTools/Properties/launchSettings.json
create mode 100644 dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-McpTools/README.md
create mode 100644 dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-McpTools/agent.manifest.yaml
create mode 100644 dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-McpTools/agent.yaml
diff --git a/dotnet/agent-framework-dotnet.slnx b/dotnet/agent-framework-dotnet.slnx
index c065430b54..12554aa8d7 100644
--- a/dotnet/agent-framework-dotnet.slnx
+++ b/dotnet/agent-framework-dotnet.slnx
@@ -289,6 +289,9 @@
+
+
+
diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-McpTools/.env.local b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-McpTools/.env.local
new file mode 100644
index 0000000000..6d7831229d
--- /dev/null
+++ b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-McpTools/.env.local
@@ -0,0 +1,4 @@
+AZURE_AI_PROJECT_ENDPOINT=
+ASPNETCORE_URLS=http://+:8088
+ASPNETCORE_ENVIRONMENT=Development
+AZURE_AI_MODEL_DEPLOYMENT_NAME=gpt-4o
diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-McpTools/Dockerfile b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-McpTools/Dockerfile
new file mode 100644
index 0000000000..fe7fceb685
--- /dev/null
+++ b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-McpTools/Dockerfile
@@ -0,0 +1,17 @@
+# Use the official .NET 10.0 ASP.NET runtime as a parent image
+FROM mcr.microsoft.com/dotnet/aspnet:10.0 AS base
+WORKDIR /app
+
+FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build
+WORKDIR /src
+COPY . .
+RUN dotnet restore
+RUN dotnet publish -c Release -o /app/publish
+
+# Final stage
+FROM base AS final
+WORKDIR /app
+COPY --from=build /app/publish .
+EXPOSE 8088
+ENV ASPNETCORE_URLS=http://+:8088
+ENTRYPOINT ["dotnet", "HostedMcpTools.dll"]
diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-McpTools/Dockerfile.contributor b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-McpTools/Dockerfile.contributor
new file mode 100644
index 0000000000..51c8c347d8
--- /dev/null
+++ b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-McpTools/Dockerfile.contributor
@@ -0,0 +1,18 @@
+# Dockerfile for contributors building from the agent-framework repository source.
+#
+# This project uses ProjectReference to the local source, which means a standard
+# multi-stage Docker build cannot resolve dependencies outside this folder.
+# Pre-publish the app targeting the container runtime and copy the output:
+#
+# dotnet publish -c Debug -f net10.0 -r linux-musl-x64 --self-contained false -o out
+# docker build -f Dockerfile.contributor -t hosted-mcp-tools .
+# docker run --rm -p 8088:8088 -e AGENT_NAME=mcp-tools -e GITHUB_PAT=$GITHUB_PAT -e AZURE_BEARER_TOKEN=$AZURE_BEARER_TOKEN --env-file .env hosted-mcp-tools
+#
+# For end-users consuming the NuGet package (not ProjectReference), use the standard
+# Dockerfile which performs a full dotnet restore + publish inside the container.
+FROM mcr.microsoft.com/dotnet/aspnet:10.0-alpine AS final
+WORKDIR /app
+COPY out/ .
+EXPOSE 8088
+ENV ASPNETCORE_URLS=http://+:8088
+ENTRYPOINT ["dotnet", "HostedMcpTools.dll"]
diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-McpTools/HostedMcpTools.csproj b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-McpTools/HostedMcpTools.csproj
new file mode 100644
index 0000000000..9ce19dd540
--- /dev/null
+++ b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-McpTools/HostedMcpTools.csproj
@@ -0,0 +1,31 @@
+
+
+
+ net10.0
+ enable
+ enable
+ false
+ HostedMcpTools
+ HostedMcpTools
+ $(NoWarn);
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-McpTools/Program.cs b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-McpTools/Program.cs
new file mode 100644
index 0000000000..a969b75477
--- /dev/null
+++ b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-McpTools/Program.cs
@@ -0,0 +1,130 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+// This sample demonstrates a hosted agent with two layers of MCP (Model Context Protocol) tools:
+//
+// 1. CLIENT-SIDE MCP: The agent connects to the Microsoft Learn MCP server directly via
+// McpClient, discovers tools, and handles tool invocations locally within the agent process.
+//
+// 2. SERVER-SIDE MCP: The agent declares a HostedMcpServerTool for the same MCP server which
+// delegates tool discovery and invocation to the LLM provider (Azure OpenAI Responses API).
+// The provider calls the MCP server on behalf of the agent — no local connection needed.
+//
+// Both patterns use the Microsoft Learn MCP server to illustrate the architectural difference:
+// client-side tools are resolved and invoked by the agent, while server-side tools are resolved
+// and invoked by the LLM provider.
+
+#pragma warning disable MEAI001 // HostedMcpServerTool is experimental
+
+using Azure.AI.Projects;
+using Azure.Core;
+using Azure.Identity;
+using DotNetEnv;
+using Microsoft.Agents.AI;
+using Microsoft.Agents.AI.Foundry.Hosting;
+using Microsoft.Extensions.AI;
+using ModelContextProtocol.Client;
+
+// Load .env file if present (for local development)
+Env.TraversePath().Load();
+
+var projectEndpoint = new Uri(Environment.GetEnvironmentVariable("AZURE_AI_PROJECT_ENDPOINT")
+ ?? throw new InvalidOperationException("AZURE_AI_PROJECT_ENDPOINT is not set."));
+var deployment = Environment.GetEnvironmentVariable("AZURE_AI_MODEL_DEPLOYMENT_NAME") ?? "gpt-4o";
+
+// Use a chained credential: try a temporary dev token first (for local Docker debugging),
+// then fall back to DefaultAzureCredential (for local dev via dotnet run / managed identity in production).
+TokenCredential credential = new ChainedTokenCredential(
+ new DevTemporaryTokenCredential(),
+ new DefaultAzureCredential());
+
+// ── Client-side MCP: Microsoft Learn (local resolution) ──────────────────────
+// Connect directly to the MCP server. The agent discovers and invokes tools locally.
+Console.WriteLine("Connecting to Microsoft Learn MCP server (client-side)...");
+
+await using var learnMcp = await McpClient.CreateAsync(new HttpClientTransport(new()
+{
+ Endpoint = new Uri("https://learn.microsoft.com/api/mcp"),
+ Name = "Microsoft Learn (client)",
+}));
+
+var clientTools = await learnMcp.ListToolsAsync();
+Console.WriteLine($"Client-side MCP tools: {string.Join(", ", clientTools.Select(t => t.Name))}");
+
+// ── Server-side MCP: Microsoft Learn (provider resolution) ───────────────────
+// Declare a HostedMcpServerTool — the LLM provider (Responses API) handles tool
+// invocations directly. No local MCP connection needed for this pattern.
+AITool serverTool = new HostedMcpServerTool(
+ serverName: "microsoft_learn_hosted",
+ serverAddress: "https://learn.microsoft.com/api/mcp")
+{
+ AllowedTools = ["microsoft_docs_search"],
+ ApprovalMode = HostedMcpServerToolApprovalMode.NeverRequire
+};
+Console.WriteLine("Server-side MCP tool: microsoft_docs_search (via HostedMcpServerTool)");
+
+// ── Combine both tool types into a single agent ──────────────────────────────
+// The agent has access to tools from both MCP patterns simultaneously.
+List allTools = [.. clientTools.Cast(), serverTool];
+
+AIAgent agent = new AIProjectClient(projectEndpoint, credential)
+ .AsAIAgent(
+ model: deployment,
+ instructions: """
+ You are a helpful developer assistant with access to Microsoft Learn documentation.
+ Use the available tools to search and retrieve documentation.
+ Be concise and provide direct answers with relevant links.
+ """,
+ name: "mcp-tools",
+ description: "Developer assistant with dual-layer MCP tools (client-side and server-side)",
+ tools: allTools);
+
+// Host the agent as a Foundry Hosted Agent using the Responses API.
+var builder = WebApplication.CreateBuilder(args);
+builder.Services.AddFoundryResponses(agent);
+
+var app = builder.Build();
+app.MapFoundryResponses();
+
+// In Development, also map the OpenAI-compatible route that AIProjectClient uses.
+if (app.Environment.IsDevelopment())
+{
+ app.MapFoundryResponses("openai/v1");
+}
+
+app.Run();
+
+///
+/// A for local Docker debugging only.
+/// Reads a pre-fetched bearer token from the AZURE_BEARER_TOKEN environment variable
+/// once at startup. This should NOT be used in production.
+///
+/// Generate a token on your host and pass it to the container:
+/// export AZURE_BEARER_TOKEN=$(az account get-access-token --resource https://ai.azure.com --query accessToken -o tsv)
+/// docker run -e AZURE_BEARER_TOKEN=$AZURE_BEARER_TOKEN ...
+///
+internal sealed class DevTemporaryTokenCredential : TokenCredential
+{
+ private const string EnvironmentVariable = "AZURE_BEARER_TOKEN";
+ private readonly string? _token;
+
+ public DevTemporaryTokenCredential()
+ {
+ _token = Environment.GetEnvironmentVariable(EnvironmentVariable);
+ }
+
+ public override AccessToken GetToken(TokenRequestContext requestContext, CancellationToken cancellationToken)
+ => GetAccessToken();
+
+ public override ValueTask GetTokenAsync(TokenRequestContext requestContext, CancellationToken cancellationToken)
+ => new(GetAccessToken());
+
+ private AccessToken GetAccessToken()
+ {
+ if (string.IsNullOrEmpty(_token))
+ {
+ throw new CredentialUnavailableException($"{EnvironmentVariable} environment variable is not set.");
+ }
+
+ return new AccessToken(_token, DateTimeOffset.UtcNow.AddHours(1));
+ }
+}
diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-McpTools/Properties/launchSettings.json b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-McpTools/Properties/launchSettings.json
new file mode 100644
index 0000000000..3042eb4d44
--- /dev/null
+++ b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-McpTools/Properties/launchSettings.json
@@ -0,0 +1,11 @@
+{
+ "profiles": {
+ "HostedMcpTools": {
+ "commandName": "Project",
+ "environmentVariables": {
+ "ASPNETCORE_ENVIRONMENT": "Development"
+ },
+ "applicationUrl": "http://localhost:8088"
+ }
+ }
+}
diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-McpTools/README.md b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-McpTools/README.md
new file mode 100644
index 0000000000..0990dfc6bd
--- /dev/null
+++ b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-McpTools/README.md
@@ -0,0 +1,86 @@
+# Hosted-McpTools
+
+A hosted agent demonstrating **two layers of MCP (Model Context Protocol) tool integration**:
+
+1. **Client-side MCP (GitHub)** — The agent connects directly to the GitHub MCP server via `McpClient`, discovers tools, and handles tool invocations locally within the agent process.
+
+2. **Server-side MCP (Microsoft Learn)** — The agent declares a `HostedMcpServerTool` which delegates tool discovery and invocation to the LLM provider (Azure OpenAI Responses API). The provider calls the MCP server on behalf of the agent with no local connection needed.
+
+## How the two MCP patterns differ
+
+| | Client-side MCP | Server-side MCP |
+|---|---|---|
+| **Connection** | Agent connects to MCP server directly | LLM provider connects to MCP server |
+| **Tool invocation** | Handled by the agent process | Handled by the Responses API |
+| **Auth** | Agent manages credentials (e.g., GitHub PAT) | Provider manages credentials |
+| **Use case** | Custom/private MCP servers, fine-grained control | Public MCP servers, simpler setup |
+| **Example** | GitHub (`McpClient` + `HttpClientTransport`) | Microsoft Learn (`HostedMcpServerTool`) |
+
+## Prerequisites
+
+- [.NET 10 SDK](https://dotnet.microsoft.com/download/dotnet/10.0)
+- An Azure AI Foundry project with a deployed model (e.g., `gpt-4o`)
+- Azure CLI logged in (`az login`)
+- A **GitHub Personal Access Token** (create at https://github.com/settings/tokens)
+
+## Configuration
+
+Copy the template and fill in your values:
+
+```bash
+cp .env.local .env
+```
+
+Edit `.env`:
+
+```env
+AZURE_AI_PROJECT_ENDPOINT=https://.services.ai.azure.com/api/projects/
+AZURE_AI_MODEL_DEPLOYMENT_NAME=gpt-4o
+GITHUB_PAT=ghp_your_token_here
+```
+
+## Running directly (contributors)
+
+```bash
+cd dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-McpTools
+dotnet run
+```
+
+### Test it
+
+Using the Azure Developer CLI:
+
+```bash
+# Uses GitHub MCP (client-side)
+azd ai agent invoke --local "Search for the agent-framework repository on GitHub"
+
+# Uses Microsoft Learn MCP (server-side)
+azd ai agent invoke --local "How do I create an Azure storage account using az cli?"
+```
+
+## Running with Docker
+
+### 1. Publish for the container runtime
+
+```bash
+dotnet publish -c Debug -f net10.0 -r linux-musl-x64 --self-contained false -o out
+```
+
+### 2. Build and run
+
+```bash
+docker build -f Dockerfile.contributor -t hosted-mcp-tools .
+
+export AZURE_BEARER_TOKEN=$(az account get-access-token --resource https://ai.azure.com --query accessToken -o tsv)
+
+docker run --rm -p 8088:8088 \
+ -e AGENT_NAME=mcp-tools \
+ -e GITHUB_PAT=$GITHUB_PAT \
+ -e AZURE_BEARER_TOKEN=$AZURE_BEARER_TOKEN \
+ --env-file .env \
+ hosted-mcp-tools
+```
+
+## NuGet package users
+
+Use the standard `Dockerfile` instead of `Dockerfile.contributor`. See the commented section in `HostedMcpTools.csproj` for the `PackageReference` alternative.
diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-McpTools/agent.manifest.yaml b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-McpTools/agent.manifest.yaml
new file mode 100644
index 0000000000..d5952940b0
--- /dev/null
+++ b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-McpTools/agent.manifest.yaml
@@ -0,0 +1,30 @@
+# yaml-language-server: $schema=https://raw.githubusercontent.com/microsoft/AgentSchema/refs/heads/main/schemas/v1.0/AgentManifest.yaml
+name: mcp-tools
+displayName: "MCP Tools Agent"
+
+description: >
+ A developer assistant demonstrating dual-layer MCP integration:
+ client-side GitHub MCP tools handled by the agent and server-side
+ Microsoft Learn MCP tools delegated to the LLM provider.
+
+metadata:
+ tags:
+ - AI Agent Hosting
+ - Azure AI AgentServer
+ - Responses Protocol
+ - Agent Framework
+ - MCP
+ - Model Context Protocol
+
+template:
+ name: mcp-tools
+ kind: hosted
+ protocols:
+ - protocol: responses
+ version: 1.0.0
+ resources:
+ cpu: "0.25"
+ memory: 0.5Gi
+parameters:
+ properties: []
+resources: []
diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-McpTools/agent.yaml b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-McpTools/agent.yaml
new file mode 100644
index 0000000000..34beb3e2c9
--- /dev/null
+++ b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-McpTools/agent.yaml
@@ -0,0 +1,9 @@
+# yaml-language-server: $schema=https://raw.githubusercontent.com/microsoft/AgentSchema/refs/heads/main/schemas/v1.0/ContainerAgent.yaml
+kind: hosted
+name: mcp-tools
+protocols:
+ - protocol: responses
+ version: 1.0.0
+resources:
+ cpu: "0.25"
+ memory: 0.5Gi