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