From 0bcb47ab8fe66de4a1fbf45307afea876c50bbc9 Mon Sep 17 00:00:00 2001 From: "Ricardo Minguez Pablos (RIDO)" Date: Mon, 18 May 2026 19:52:54 -0700 Subject: [PATCH 1/9] checkpoint --- .gitignore | 5 + dotnet/work-iq-teams-bot/nuget.config | 9 ++ .../work-iq-teams-bot.AppHost/AppHost.cs | 9 ++ .../appsettings.json | 9 ++ .../work-iq-teams-bot.AppHost.csproj | 15 ++ .../Extensions.cs | 130 ++++++++++++++++ .../work-iq-teams-bot.ServiceDefaults.csproj | 22 +++ .../IConversationHistoryStore.cs | 69 +++++++++ .../work-iq-teams-bot.TeamsApp/Program.cs | 19 +++ .../WorkIQAgent.McpClientFactory.cs | 63 ++++++++ .../WorkIQAgent.Options.cs | 24 +++ .../WorkIQAgent.ServiceExtensions.cs | 78 ++++++++++ .../work-iq-teams-bot.TeamsApp/WorkIQAgent.cs | 143 ++++++++++++++++++ .../WorkIQTeamsBotApp.cs | 60 ++++++++ .../appsettings.json | 9 ++ .../work-iq-teams-bot.TeamsApp.csproj | 28 ++++ .../work-iq-teams-bot/work-iq-teams-bot.sln | 37 +++++ .../work-iq-teams-bot/work-iq-teams-bot.slnx | 6 + 18 files changed, 735 insertions(+) create mode 100644 dotnet/work-iq-teams-bot/nuget.config create mode 100644 dotnet/work-iq-teams-bot/work-iq-teams-bot.AppHost/AppHost.cs create mode 100644 dotnet/work-iq-teams-bot/work-iq-teams-bot.AppHost/appsettings.json create mode 100644 dotnet/work-iq-teams-bot/work-iq-teams-bot.AppHost/work-iq-teams-bot.AppHost.csproj create mode 100644 dotnet/work-iq-teams-bot/work-iq-teams-bot.ServiceDefaults/Extensions.cs create mode 100644 dotnet/work-iq-teams-bot/work-iq-teams-bot.ServiceDefaults/work-iq-teams-bot.ServiceDefaults.csproj create mode 100644 dotnet/work-iq-teams-bot/work-iq-teams-bot.TeamsApp/IConversationHistoryStore.cs create mode 100644 dotnet/work-iq-teams-bot/work-iq-teams-bot.TeamsApp/Program.cs create mode 100644 dotnet/work-iq-teams-bot/work-iq-teams-bot.TeamsApp/WorkIQAgent.McpClientFactory.cs create mode 100644 dotnet/work-iq-teams-bot/work-iq-teams-bot.TeamsApp/WorkIQAgent.Options.cs create mode 100644 dotnet/work-iq-teams-bot/work-iq-teams-bot.TeamsApp/WorkIQAgent.ServiceExtensions.cs create mode 100644 dotnet/work-iq-teams-bot/work-iq-teams-bot.TeamsApp/WorkIQAgent.cs create mode 100644 dotnet/work-iq-teams-bot/work-iq-teams-bot.TeamsApp/WorkIQTeamsBotApp.cs create mode 100644 dotnet/work-iq-teams-bot/work-iq-teams-bot.TeamsApp/appsettings.json create mode 100644 dotnet/work-iq-teams-bot/work-iq-teams-bot.TeamsApp/work-iq-teams-bot.TeamsApp.csproj create mode 100644 dotnet/work-iq-teams-bot/work-iq-teams-bot.sln create mode 100644 dotnet/work-iq-teams-bot/work-iq-teams-bot.slnx diff --git a/.gitignore b/.gitignore index ab02d105..e0287e7a 100644 --- a/.gitignore +++ b/.gitignore @@ -116,6 +116,11 @@ a365.generated.config.json app.zip publish/ +# Sensitive / per-developer config +**/Properties/launchSettings.json +**/appsettings.Development.json +*.csproj.user + # OS-specific files .DS_Store Thumbs.db diff --git a/dotnet/work-iq-teams-bot/nuget.config b/dotnet/work-iq-teams-bot/nuget.config new file mode 100644 index 00000000..562fe6a4 --- /dev/null +++ b/dotnet/work-iq-teams-bot/nuget.config @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/dotnet/work-iq-teams-bot/work-iq-teams-bot.AppHost/AppHost.cs b/dotnet/work-iq-teams-bot/work-iq-teams-bot.AppHost/AppHost.cs new file mode 100644 index 00000000..18cae944 --- /dev/null +++ b/dotnet/work-iq-teams-bot/work-iq-teams-bot.AppHost/AppHost.cs @@ -0,0 +1,9 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +var builder = DistributedApplication.CreateBuilder(args); + +builder.AddProject("teamsbotapp") + .WithHttpHealthCheck("/health"); + +builder.Build().Run(); diff --git a/dotnet/work-iq-teams-bot/work-iq-teams-bot.AppHost/appsettings.json b/dotnet/work-iq-teams-bot/work-iq-teams-bot.AppHost/appsettings.json new file mode 100644 index 00000000..31c092aa --- /dev/null +++ b/dotnet/work-iq-teams-bot/work-iq-teams-bot.AppHost/appsettings.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning", + "Aspire.Hosting.Dcp": "Warning" + } + } +} diff --git a/dotnet/work-iq-teams-bot/work-iq-teams-bot.AppHost/work-iq-teams-bot.AppHost.csproj b/dotnet/work-iq-teams-bot/work-iq-teams-bot.AppHost/work-iq-teams-bot.AppHost.csproj new file mode 100644 index 00000000..46c1535f --- /dev/null +++ b/dotnet/work-iq-teams-bot/work-iq-teams-bot.AppHost/work-iq-teams-bot.AppHost.csproj @@ -0,0 +1,15 @@ + + + + Exe + net10.0 + enable + enable + 7163dbbd-0055-4d5d-a0c0-1851534e6203 + + + + + + + diff --git a/dotnet/work-iq-teams-bot/work-iq-teams-bot.ServiceDefaults/Extensions.cs b/dotnet/work-iq-teams-bot/work-iq-teams-bot.ServiceDefaults/Extensions.cs new file mode 100644 index 00000000..d6821fe9 --- /dev/null +++ b/dotnet/work-iq-teams-bot/work-iq-teams-bot.ServiceDefaults/Extensions.cs @@ -0,0 +1,130 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Diagnostics.HealthChecks; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Diagnostics.HealthChecks; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.ServiceDiscovery; +using OpenTelemetry; +using OpenTelemetry.Metrics; +using OpenTelemetry.Trace; + +namespace Microsoft.Extensions.Hosting; + +// Adds common Aspire services: service discovery, resilience, health checks, and OpenTelemetry. +// This project should be referenced by each service project in your solution. +// To learn more about using this project, see https://aka.ms/dotnet/aspire/service-defaults +public static class Extensions +{ + private const string HealthEndpointPath = "/health"; + private const string AlivenessEndpointPath = "/alive"; + + public static TBuilder AddServiceDefaults(this TBuilder builder) where TBuilder : IHostApplicationBuilder + { + builder.ConfigureOpenTelemetry(); + + builder.AddDefaultHealthChecks(); + + builder.Services.AddServiceDiscovery(); + + builder.Services.ConfigureHttpClientDefaults(http => + { + // Turn on resilience by default + http.AddStandardResilienceHandler(); + + // Turn on service discovery by default + http.AddServiceDiscovery(); + }); + + // Uncomment the following to restrict the allowed schemes for service discovery. + // builder.Services.Configure(options => + // { + // options.AllowedSchemes = ["https"]; + // }); + + return builder; + } + + public static TBuilder ConfigureOpenTelemetry(this TBuilder builder) where TBuilder : IHostApplicationBuilder + { + builder.Logging.AddOpenTelemetry(logging => + { + logging.IncludeFormattedMessage = true; + logging.IncludeScopes = true; + }); + + builder.Services.AddOpenTelemetry() + .WithMetrics(metrics => + { + metrics.AddAspNetCoreInstrumentation() + .AddHttpClientInstrumentation() + .AddRuntimeInstrumentation(); + }) + .WithTracing(tracing => + { + tracing.AddSource(builder.Environment.ApplicationName) + .AddAspNetCoreInstrumentation(tracing => + // Exclude health check requests from tracing + tracing.Filter = context => + !context.Request.Path.StartsWithSegments(HealthEndpointPath) + && !context.Request.Path.StartsWithSegments(AlivenessEndpointPath) + ) + // Uncomment the following line to enable gRPC instrumentation (requires the OpenTelemetry.Instrumentation.GrpcNetClient package) + //.AddGrpcClientInstrumentation() + .AddHttpClientInstrumentation(); + }); + + //builder.AddOpenTelemetryExporters(); + + return builder; + } + + private static TBuilder AddOpenTelemetryExporters(this TBuilder builder) where TBuilder : IHostApplicationBuilder + { + var useOtlpExporter = !string.IsNullOrWhiteSpace(builder.Configuration["OTEL_EXPORTER_OTLP_ENDPOINT"]); + + if (useOtlpExporter) + { + builder.Services.AddOpenTelemetry().UseOtlpExporter(); + } + + // Uncomment the following lines to enable the Azure Monitor exporter (requires the Azure.Monitor.OpenTelemetry.AspNetCore package) + //if (!string.IsNullOrEmpty(builder.Configuration["APPLICATIONINSIGHTS_CONNECTION_STRING"])) + //{ + // builder.Services.AddOpenTelemetry() + // .UseAzureMonitor(); + //} + + return builder; + } + + public static TBuilder AddDefaultHealthChecks(this TBuilder builder) where TBuilder : IHostApplicationBuilder + { + builder.Services.AddHealthChecks() + // Add a default liveness check to ensure app is responsive + .AddCheck("self", () => HealthCheckResult.Healthy(), ["live"]); + + return builder; + } + + public static WebApplication MapDefaultEndpoints(this WebApplication app) + { + // Adding health checks endpoints to applications in non-development environments has security implications. + // See https://aka.ms/dotnet/aspire/healthchecks for details before enabling these endpoints in non-development environments. + if (app.Environment.IsDevelopment()) + { + // All health checks must pass for app to be considered ready to accept traffic after starting + app.MapHealthChecks(HealthEndpointPath); + + // Only health checks tagged with the "live" tag must pass for app to be considered alive + app.MapHealthChecks(AlivenessEndpointPath, new HealthCheckOptions + { + Predicate = r => r.Tags.Contains("live") + }); + } + + return app; + } +} diff --git a/dotnet/work-iq-teams-bot/work-iq-teams-bot.ServiceDefaults/work-iq-teams-bot.ServiceDefaults.csproj b/dotnet/work-iq-teams-bot/work-iq-teams-bot.ServiceDefaults/work-iq-teams-bot.ServiceDefaults.csproj new file mode 100644 index 00000000..791c654e --- /dev/null +++ b/dotnet/work-iq-teams-bot/work-iq-teams-bot.ServiceDefaults/work-iq-teams-bot.ServiceDefaults.csproj @@ -0,0 +1,22 @@ + + + + net10.0 + enable + enable + true + + + + + + + + + + + + + + + diff --git a/dotnet/work-iq-teams-bot/work-iq-teams-bot.TeamsApp/IConversationHistoryStore.cs b/dotnet/work-iq-teams-bot/work-iq-teams-bot.TeamsApp/IConversationHistoryStore.cs new file mode 100644 index 00000000..d7ba4c1e --- /dev/null +++ b/dotnet/work-iq-teams-bot/work-iq-teams-bot.TeamsApp/IConversationHistoryStore.cs @@ -0,0 +1,69 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using Microsoft.Extensions.AI; +using System.Collections.Concurrent; + +namespace work_iq_teams_bot.TeamsApp; + +/// +/// Stores per-conversation chat history and provides a serialization gate so +/// concurrent turns within the same conversation cannot interleave history mutations. +/// Implementations are expected to be registered as a singleton, since per-conversation +/// state must outlive any individual turn. +/// +internal interface IConversationHistoryStore +{ + /// + /// Returns the conversation's chat history, creating it from on first access. + /// + /// The conversation identifier. + /// Initial messages (typically a system prompt) used only on first access. + List GetOrCreateHistory(string conversationId, Func> seed); + + /// + /// Acquires the per-conversation serialization gate. Dispose the returned handle to release. + /// + Task AcquireGateAsync(string conversationId, CancellationToken cancellationToken); +} + +/// +/// Process-local backed by . +/// History grows unbounded for the lifetime of the process; replace with a distributed/bounded store for production. +/// +internal sealed class InMemoryConversationHistoryStore : IConversationHistoryStore +{ + private readonly ConcurrentDictionary> _histories = new(); + private readonly ConcurrentDictionary _locks = new(); + + public List GetOrCreateHistory(string conversationId, Func> seed) + { + ArgumentException.ThrowIfNullOrEmpty(conversationId); + ArgumentNullException.ThrowIfNull(seed); + + return _histories.GetOrAdd(conversationId, _ => [.. seed()]); + } + + public async Task AcquireGateAsync(string conversationId, CancellationToken cancellationToken) + { + ArgumentException.ThrowIfNullOrEmpty(conversationId); + + SemaphoreSlim gate = _locks.GetOrAdd(conversationId, _ => new SemaphoreSlim(1, 1)); + await gate.WaitAsync(cancellationToken).ConfigureAwait(false); + return new GateHandle(gate); + } + + private sealed class GateHandle(SemaphoreSlim gate) : IAsyncDisposable + { + private int _released; + + public ValueTask DisposeAsync() + { + if (Interlocked.Exchange(ref _released, 1) == 0) + { + gate.Release(); + } + return ValueTask.CompletedTask; + } + } +} diff --git a/dotnet/work-iq-teams-bot/work-iq-teams-bot.TeamsApp/Program.cs b/dotnet/work-iq-teams-bot/work-iq-teams-bot.TeamsApp/Program.cs new file mode 100644 index 00000000..a9e01fa3 --- /dev/null +++ b/dotnet/work-iq-teams-bot/work-iq-teams-bot.TeamsApp/Program.cs @@ -0,0 +1,19 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using work_iq_teams_bot.TeamsApp; +using Microsoft.Teams.Apps; + +WebApplicationBuilder builder = WebApplication.CreateSlimBuilder(args); + +builder.AddServiceDefaults(); + +builder.Services + .AddWorkIQAgent(builder.Configuration) + .AddTeamsBotApplication(); + +WebApplication webApp = builder.Build(); + +webApp.MapDefaultEndpoints(); +webApp.UseTeamsBotApplication(); +webApp.Run(); diff --git a/dotnet/work-iq-teams-bot/work-iq-teams-bot.TeamsApp/WorkIQAgent.McpClientFactory.cs b/dotnet/work-iq-teams-bot/work-iq-teams-bot.TeamsApp/WorkIQAgent.McpClientFactory.cs new file mode 100644 index 00000000..e95cadf5 --- /dev/null +++ b/dotnet/work-iq-teams-bot/work-iq-teams-bot.TeamsApp/WorkIQAgent.McpClientFactory.cs @@ -0,0 +1,63 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using Microsoft.Identity.Abstractions; +using Microsoft.Identity.Web; +using Microsoft.Teams.Core.Schema; +using ModelContextProtocol.Client; + +namespace work_iq_teams_bot.TeamsApp; + +/// +/// Creates authenticated instances using the SDK's +/// to attach +/// user-delegated tokens to outbound MCP HTTP requests. +/// +internal sealed class WorkIQAgentMcpClientFactory( + IAuthorizationHeaderProvider authorizationHeaderProvider, + ILoggerFactory loggerFactory) +{ + private const string WorkIQScopeForMCPServers = "ea9ffc3e-8a23-4a7d-836d-234d7c7565c1/.default"; + + public async Task CreateClientAsync(string serverUrl, AgenticIdentity agenticIdentity, CancellationToken cancellationToken = default) + { + string token = await AcquireTokenAsync(agenticIdentity, cancellationToken).ConfigureAwait(false); + + return await McpClient.CreateAsync( + new HttpClientTransport(new() + { + Endpoint = new Uri(serverUrl), + Name = "WorkIQ Agent", + AdditionalHeaders = new Dictionary { ["Authorization"] = $"Bearer {token}" } + }), + loggerFactory: loggerFactory, + cancellationToken: cancellationToken).ConfigureAwait(false); + } + + private async Task AcquireTokenAsync(AgenticIdentity agenticIdentity, CancellationToken cancellationToken) + { + ArgumentNullException.ThrowIfNullOrEmpty(agenticIdentity.AgenticAppId); + ArgumentNullException.ThrowIfNullOrEmpty(agenticIdentity.AgenticUserId); + + if (!Guid.TryParse(agenticIdentity.AgenticUserId, out Guid agenticUserGuid)) + { + throw new InvalidOperationException($"Invalid AgenticUserId '{agenticIdentity.AgenticUserId}'."); + } + + AuthorizationHeaderProviderOptions options = new AuthorizationHeaderProviderOptions() + { + AcquireTokenOptions = new() + { + AuthenticationOptionsName = "AzureAd", + } + }.WithAgentUserIdentity(agenticIdentity.AgenticAppId, agenticUserGuid); + + string header = await authorizationHeaderProvider.CreateAuthorizationHeaderAsync( + [WorkIQScopeForMCPServers], options, cancellationToken: cancellationToken).ConfigureAwait(false); + + // Strip "Bearer " prefix if present + return header.StartsWith("Bearer ", StringComparison.OrdinalIgnoreCase) + ? header["Bearer ".Length..] + : header; + } +} diff --git a/dotnet/work-iq-teams-bot/work-iq-teams-bot.TeamsApp/WorkIQAgent.Options.cs b/dotnet/work-iq-teams-bot/work-iq-teams-bot.TeamsApp/WorkIQAgent.Options.cs new file mode 100644 index 00000000..a89d5773 --- /dev/null +++ b/dotnet/work-iq-teams-bot/work-iq-teams-bot.TeamsApp/WorkIQAgent.Options.cs @@ -0,0 +1,24 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +namespace work_iq_teams_bot.TeamsApp; + +/// +/// Configuration options for the , including +/// the MCP server endpoints to connect to. +/// +internal sealed class WorkIQAgentOptions +{ + public const string SectionName = "WorkIQAgent"; + + /// + /// The MCP server URLs the agent connects to for tool discovery. + /// + public string[] McpServerUrls { get; set; } = + [ + "https://agent365.svc.cloud.microsoft/agents/servers/mcp_TeamsServer", + "https://agent365.svc.cloud.microsoft/agents/servers/mcp_MailTools", + "https://agent365.svc.cloud.microsoft/agents/servers/mcp_CalendarTools", + "https://agent365.svc.cloud.microsoft/agents/servers/mcp_MeServer", + ]; +} diff --git a/dotnet/work-iq-teams-bot/work-iq-teams-bot.TeamsApp/WorkIQAgent.ServiceExtensions.cs b/dotnet/work-iq-teams-bot/work-iq-teams-bot.TeamsApp/WorkIQAgent.ServiceExtensions.cs new file mode 100644 index 00000000..3c7a9992 --- /dev/null +++ b/dotnet/work-iq-teams-bot/work-iq-teams-bot.TeamsApp/WorkIQAgent.ServiceExtensions.cs @@ -0,0 +1,78 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using Azure.AI.OpenAI; +using Microsoft.Extensions.AI; +using Microsoft.Identity.Web.TokenCacheProviders; +using Microsoft.Identity.Web.TokenCacheProviders.Distributed; +using Microsoft.OpenTelemetry; +using Microsoft.Teams.Apps.Diagnostics; +using Microsoft.Teams.Core.Diagnostics; +using OpenTelemetry; +using OpenTelemetry.Resources; +using System.ClientModel; + +namespace work_iq_teams_bot.TeamsApp; + +/// +/// Extension methods for registering the Agent, its dependencies, and the +/// custom with DI. +/// +internal static class WorkIQServiceExtensions +{ + /// + /// Registers , , , + /// , and with the service collection. + /// + public static IServiceCollection AddWorkIQAgent(this IServiceCollection services, IConfiguration configuration) + { + services.Configure(configuration.GetSection(WorkIQAgentOptions.SectionName)); + + services.AddChatClient(sp => + { + IConfiguration config = sp.GetRequiredService(); + string endpoint = config["AzureOpenAI:Endpoint"] ?? throw new InvalidOperationException("AzureOpenAI:Endpoint is required."); + string apiKey = config["AzureOpenAI:ApiKey"] ?? throw new InvalidOperationException("AzureOpenAI:ApiKey is required."); + string modelId = config["AzureOpenAI:ModelId"] ?? throw new InvalidOperationException("AzureOpenAI:ModelId is required."); + + return new AzureOpenAIClient(new Uri(endpoint), new ApiKeyCredential(apiKey)) + .GetChatClient(modelId) + .AsIChatClient(); + }) + .UseFunctionInvocation() + .UseOpenTelemetry(sourceName: "Experimental.Microsoft.Extensions.AI"); + + services.AddSingleton(); + + // Conversation history must outlive any single turn -> singleton. + services.AddSingleton(); + + // Agent is a per-turn execution unit; resolved from a fresh scope inside the bot handler. + services.AddScoped(); + + // Supply a distributed token cache so MSAL does not fall back to in-memory-only caching. + // For production, replace AddDistributedMemoryCache with Redis/SQL Server. + services.AddDistributedMemoryCache(); + services.AddSingleton(); + + string[] activitySources = [CoreTelemetryNames.ActivitySourceName, TeamsBotApplicationTelemetry.ActivitySourceName, "Experimental.Microsoft.Agents.AI", "ModelContextProtocol"]; + string[] meterNames = [CoreTelemetryNames.MeterName, TeamsBotApplicationTelemetry.MeterName, "Experimental.Microsoft.Agents.AI", "ModelContextProtocol"]; + + services.AddOpenTelemetry() + .ConfigureResource(r => r + .AddService(serviceName: "WorkIQTeamsBot", serviceVersion: "0.0.1") + .AddAttributes(new Dictionary + { + ["service.namespace"] = "TeamsSamples" + })) + .UseMicrosoftOpenTelemetry(o => + { + o.Exporters = ExportTarget.Otlp | ExportTarget.AzureMonitor; + o.Instrumentation.EnableHttpClientInstrumentation = true; + o.Instrumentation.EnableAspNetCoreInstrumentation = true; + }) + .WithTracing(t => t.AddSource(activitySources)) + .WithMetrics(m => m.AddMeter(meterNames)); + return services; + } +} diff --git a/dotnet/work-iq-teams-bot/work-iq-teams-bot.TeamsApp/WorkIQAgent.cs b/dotnet/work-iq-teams-bot/work-iq-teams-bot.TeamsApp/WorkIQAgent.cs new file mode 100644 index 00000000..f7f4d46a --- /dev/null +++ b/dotnet/work-iq-teams-bot/work-iq-teams-bot.TeamsApp/WorkIQAgent.cs @@ -0,0 +1,143 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using Microsoft.Extensions.AI; +using Microsoft.Extensions.Options; +using Microsoft.Teams.Apps.Schema; +using Microsoft.Teams.Core.Schema; +using ModelContextProtocol.Client; + +namespace work_iq_teams_bot.TeamsApp; + +/// +/// Per-turn agent that resolves MCP tools, replays the conversation through an , +/// and returns the assistant's reply. Conversation history is owned by +/// so this type can safely be registered as a scoped service. +/// +internal class WorkIQAgent( + IChatClient chatClient, + WorkIQAgentMcpClientFactory mcpClientFactory, + IConversationHistoryStore historyStore, + IOptions options) +{ + private const string SystemPrompt = """ + You are a Teams assistant that can use the MCP Teams tools to send messages to users, channels, and meetings, + the MCP Mail tools to read and send emails, the MCP Calendar tools to manage calendar events, + and the MCP Me tools to access user profile information. + """; + + /// + /// Maximum number of non-system messages retained per conversation. When exceeded the + /// oldest messages (after the system prompt) are trimmed. Keeps the token budget under + /// control for long-running conversations. + /// + private const int MaxHistoryMessages = 50; + + public async Task RunAsync( + MessageActivity activity, + CancellationToken cancellationToken) + { + ArgumentException.ThrowIfNullOrEmpty(activity.Conversation?.Id); + ArgumentNullException.ThrowIfNull(activity.Recipient); + + string[] serverUrls = options.Value.McpServerUrls; + McpClient[] mcpClients = await CreateClientsAsync(serverUrls, activity.Recipient.GetAgenticIdentity(), cancellationToken).ConfigureAwait(false); + + try + { + IList[] toolLists = await Task.WhenAll( + mcpClients.Select(c => + c.ListToolsAsync(cancellationToken: cancellationToken).AsTask())).ConfigureAwait(false); + + List allTools = [.. toolLists.SelectMany(t => t)]; + + List history = historyStore.GetOrCreateHistory( + activity.Conversation.Id, + () => [new ChatMessage(ChatRole.System, SystemPrompt),]); + + // Serialize turns within a single conversation so concurrent submits + // (e.g. clarification race) don't interleave history mutations. + await using IAsyncDisposable gate = await historyStore.AcquireGateAsync(activity.Conversation.Id, cancellationToken).ConfigureAwait(false); + + string userText = activity.TextWithoutMentions ?? string.Empty; + history.Add(new ChatMessage(ChatRole.User, $"{userText}\n\n[Turn context: {activity.ToJson()}]")); + + TrimHistory(history); + + ChatOptions chatOptions = new() + { + Tools = allTools + }; + + ChatResponse chatResponse = await chatClient.GetResponseAsync(history, chatOptions, cancellationToken).ConfigureAwait(false); + + return chatResponse.Text; + } + finally + { + await DisposeAllAsync(mcpClients).ConfigureAwait(false); + } + } + + /// + /// Creates one MCP client per server URL in parallel. If any creation fails, every + /// already-created client is disposed before the failure is rethrown so we never leak + /// transports on partial failure. + /// + private async Task CreateClientsAsync( + IReadOnlyList serverUrls, + AgenticIdentity? agentic, + CancellationToken cancellationToken) + { + ArgumentNullException.ThrowIfNull(agentic); + Task[] tasks = [.. serverUrls.Select(url => mcpClientFactory.CreateClientAsync(url, agentic, cancellationToken))]; + try + { + return await Task.WhenAll(tasks).ConfigureAwait(false); + } + catch + { + McpClient[] created = [.. tasks + .Where(t => t.Status == TaskStatus.RanToCompletion) + .Select(t => t.Result)]; + await DisposeAllAsync(created).ConfigureAwait(false); + throw; + } + } + + /// + /// Keeps the first message (system prompt) and trims the oldest non-system messages + /// when the history exceeds . + /// + private static void TrimHistory(List history) + { + // index 0 is the system prompt; everything after is conversation messages. + int conversationCount = history.Count - 1; + if (conversationCount <= MaxHistoryMessages) + { + return; + } + + int excess = conversationCount - MaxHistoryMessages; + history.RemoveRange(1, excess); + } + + /// + /// Best-effort dispose of every client. Disposal failures are swallowed so they cannot + /// mask the real exception flowing through the surrounding finally. + /// + private static async ValueTask DisposeAllAsync(IReadOnlyList clients) + { + foreach (McpClient client in clients) + { + try + { + await client.DisposeAsync().ConfigureAwait(false); + } + catch + { + // Ignore: see method summary. + } + } + } +} diff --git a/dotnet/work-iq-teams-bot/work-iq-teams-bot.TeamsApp/WorkIQTeamsBotApp.cs b/dotnet/work-iq-teams-bot/work-iq-teams-bot.TeamsApp/WorkIQTeamsBotApp.cs new file mode 100644 index 00000000..d4b6c6ad --- /dev/null +++ b/dotnet/work-iq-teams-bot/work-iq-teams-bot.TeamsApp/WorkIQTeamsBotApp.cs @@ -0,0 +1,60 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using Microsoft.Teams.Apps; +using Microsoft.Teams.Apps.Api.Clients; +using Microsoft.Teams.Apps.Handlers; +using Microsoft.Teams.Apps.Schema; +using Microsoft.Teams.Core; +using Microsoft.Teams.Core.Hosting; + +namespace work_iq_teams_bot.TeamsApp; + +/// +/// Custom for the A365 MCP sample. Registers the +/// inbound message handler in its constructor and resolves a fresh +/// from a per-turn DI scope so scoped services (and any future scoped dependencies of +/// ) are honored correctly. +/// +internal sealed class WorkIQTeamsBotApp : TeamsBotApplication +{ + private readonly IServiceScopeFactory _scopeFactory; + + public WorkIQTeamsBotApp( + ConversationClient conversationClient, + UserTokenClient userTokenClient, + ApiClient teamsApiClient, + IHttpContextAccessor httpContextAccessor, + ILogger logger, + IServiceScopeFactory scopeFactory, + BotApplicationOptions? options = null, + TeamsBotApplicationOptions? teamsOptions = null) + : base(conversationClient, userTokenClient, teamsApiClient, httpContextAccessor, logger, options, teamsOptions) + { + _scopeFactory = scopeFactory; + + this.OnMessage(HandleMessageAsync); + } + + private async Task HandleMessageAsync(Context context, CancellationToken cancellationToken) + { + await context.SendTypingActivityAsync(cancellationToken); + + string userText = context.Activity.TextWithoutMentions ?? string.Empty; + + // Resolve Agent from a fresh per-turn scope so scoped services have a well-defined lifetime + // independent of the singleton bot application and of any ambient HTTP request scope. + await using AsyncServiceScope scope = _scopeFactory.CreateAsyncScope(); + WorkIQAgent agent = scope.ServiceProvider.GetRequiredService(); + + string response = await agent.RunAsync( + context.Activity, + cancellationToken); + + TeamsActivity reply = TeamsActivity.CreateBuilder() + .WithText(response, TextFormats.Markdown) + .Build(); + + await context.SendActivityAsync(reply, cancellationToken); + } +} diff --git a/dotnet/work-iq-teams-bot/work-iq-teams-bot.TeamsApp/appsettings.json b/dotnet/work-iq-teams-bot/work-iq-teams-bot.TeamsApp/appsettings.json new file mode 100644 index 00000000..5febf4fe --- /dev/null +++ b/dotnet/work-iq-teams-bot/work-iq-teams-bot.TeamsApp/appsettings.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Warning", + "Microsoft.Teams": "Information" + } + }, + "AllowedHosts": "*" +} diff --git a/dotnet/work-iq-teams-bot/work-iq-teams-bot.TeamsApp/work-iq-teams-bot.TeamsApp.csproj b/dotnet/work-iq-teams-bot/work-iq-teams-bot.TeamsApp/work-iq-teams-bot.TeamsApp.csproj new file mode 100644 index 00000000..dba42930 --- /dev/null +++ b/dotnet/work-iq-teams-bot/work-iq-teams-bot.TeamsApp/work-iq-teams-bot.TeamsApp.csproj @@ -0,0 +1,28 @@ + + + + net10.0 + enable + enable + + + + + + + + + + + + + + + + + + + + + + diff --git a/dotnet/work-iq-teams-bot/work-iq-teams-bot.sln b/dotnet/work-iq-teams-bot/work-iq-teams-bot.sln new file mode 100644 index 00000000..776e4a47 --- /dev/null +++ b/dotnet/work-iq-teams-bot/work-iq-teams-bot.sln @@ -0,0 +1,37 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 18 +VisualStudioVersion = 18.6.11806.211 stable +MinimumVisualStudioVersion = 17.8.0.0 +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "work-iq-teams-bot.AppHost", "work-iq-teams-bot.AppHost\work-iq-teams-bot.AppHost.csproj", "{8E10E2B4-8580-4D17-B83E-2E0286FD3BA4}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "work-iq-teams-bot.ServiceDefaults", "work-iq-teams-bot.ServiceDefaults\work-iq-teams-bot.ServiceDefaults.csproj", "{BCEF1286-B692-4111-9810-CF6642F3B133}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "work-iq-teams-bot.TeamsApp", "work-iq-teams-bot.TeamsApp\work-iq-teams-bot.TeamsApp.csproj", "{0AC5B080-D062-4AC6-9D26-73B340437E1A}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {8E10E2B4-8580-4D17-B83E-2E0286FD3BA4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {8E10E2B4-8580-4D17-B83E-2E0286FD3BA4}.Debug|Any CPU.Build.0 = Debug|Any CPU + {8E10E2B4-8580-4D17-B83E-2E0286FD3BA4}.Release|Any CPU.ActiveCfg = Release|Any CPU + {8E10E2B4-8580-4D17-B83E-2E0286FD3BA4}.Release|Any CPU.Build.0 = Release|Any CPU + {BCEF1286-B692-4111-9810-CF6642F3B133}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {BCEF1286-B692-4111-9810-CF6642F3B133}.Debug|Any CPU.Build.0 = Debug|Any CPU + {BCEF1286-B692-4111-9810-CF6642F3B133}.Release|Any CPU.ActiveCfg = Release|Any CPU + {BCEF1286-B692-4111-9810-CF6642F3B133}.Release|Any CPU.Build.0 = Release|Any CPU + {0AC5B080-D062-4AC6-9D26-73B340437E1A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {0AC5B080-D062-4AC6-9D26-73B340437E1A}.Debug|Any CPU.Build.0 = Debug|Any CPU + {0AC5B080-D062-4AC6-9D26-73B340437E1A}.Release|Any CPU.ActiveCfg = Release|Any CPU + {0AC5B080-D062-4AC6-9D26-73B340437E1A}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {AD2E809D-7A77-458A-8887-6626D8A078DE} + EndGlobalSection +EndGlobal diff --git a/dotnet/work-iq-teams-bot/work-iq-teams-bot.slnx b/dotnet/work-iq-teams-bot/work-iq-teams-bot.slnx new file mode 100644 index 00000000..02daa7c2 --- /dev/null +++ b/dotnet/work-iq-teams-bot/work-iq-teams-bot.slnx @@ -0,0 +1,6 @@ + + + + + + From eae1588864b3f6c83c378545a1b3cb33e68f1b2c Mon Sep 17 00:00:00 2001 From: "Ricardo Minguez Pablos (RIDO)" Date: Tue, 19 May 2026 08:01:34 -0700 Subject: [PATCH 2/9] scopes --- .../docs/agent-identities-fic-issue.md | 59 +++++++++++++++++++ .../WorkIQAgent.ServiceExtensions.cs | 8 ++- .../WorkIQTeamsBotApp.cs | 17 +++--- 3 files changed, 74 insertions(+), 10 deletions(-) create mode 100644 dotnet/work-iq-teams-bot/docs/agent-identities-fic-issue.md diff --git a/dotnet/work-iq-teams-bot/docs/agent-identities-fic-issue.md b/dotnet/work-iq-teams-bot/docs/agent-identities-fic-issue.md new file mode 100644 index 00000000..fb479048 --- /dev/null +++ b/dotnet/work-iq-teams-bot/docs/agent-identities-fic-issue.md @@ -0,0 +1,59 @@ +# Missing `AddAgentIdentities()` Causes AADSTS7000215 on First Turn + +## Symptom + +When the bot receives its first message, all MCP client token acquisitions fail with: + +``` +ErrorCode: invalid_client +HTTP StatusCode 401 +Microsoft Entra ID Error Code AADSTS7000215 +``` + +MSAL logs show `Token Acquisition (1007) failed` — one failure per MCP server (4 servers = 4 failures, all at the same timestamp). Subsequent turns may succeed because MSAL caches tokens from a degraded fallback path. + +## Root Cause + +`WithAgentUserIdentity()` (from `Microsoft.Identity.Web.AgentIdentities`) configures `AuthorizationHeaderProviderOptions` with: + +- `ClientId` set to the **agent application ID** (e.g., `9218bdba-...`) +- `AgentIdentityKey` and `UserIdKey` in `ExtraParameters` + +These parameters are designed for the **FIC (Federated Identity Credential)** grant flow (`grant_type=user_fic`), which is a two-step process: + +1. Acquire a FIC token for the agent app using the **blueprint's client credentials** (from the `AzureAd` config section) +2. Exchange that for a user FIC assertion scoped to the agent identity + +The MSAL add-in that implements this flow (`AgentUserIdentityMsalAddIn.OnBeforeUserFicForAgentUserIdentityAsync`) is registered by calling `services.AddAgentIdentities()`. **If this call is missing**, MSAL never hooks the FIC handler and falls back to a standard silent token acquisition where: + +- `client_id` = agent application ID (`9218bdba-...`) — set by `WithAgentUserIdentity` +- `client_secret` = the blueprint's secret (from the `AzureAd` config, which has `ClientId = 74018ebb-...`) + +Entra ID rejects this because the secret does not belong to the agent application — it belongs to the blueprint. This produces `AADSTS7000215` (`invalid_client`). + +## Fix + +Register the Agent Identities MSAL add-in in your DI setup: + +```csharp +using Microsoft.Identity.Web; + +services.AddAgentIdentities(); +``` + +This registers the `OnBeforeTokenAcquisitionForTestUserAsync` callback that intercepts token requests with agent identity parameters and rewrites them into the correct FIC grant flow. + +## Diagnostic Checklist + +| Check | Expected | +|-------|----------| +| `AddAgentIdentities()` called in DI setup | Yes | +| `AzureAd` config has blueprint's `ClientId` + `ClientSecret` | Yes | +| `WithAgentUserIdentity()` receives the agent app ID + user OID | Yes | +| `Microsoft.Identity.Web.AgentIdentities` NuGet package referenced | Yes | + +## References + +- `Microsoft.Identity.Web.AgentIdentities` source: [`AgentIdentitiesExtension.cs`](https://github.com/AzureAD/microsoft-identity-web/blob/main/src/Microsoft.Identity.Web.AgentIdentities/AgentIdentitiesExtension.cs) +- FIC grant handler: [`AgentUserIdentityMsalAddIn.cs`](https://github.com/AzureAD/microsoft-identity-web/blob/main/src/Microsoft.Identity.Web.AgentIdentities/AgentUserIdentityMsalAddIn.cs) +- Entra error reference: [AADSTS7000215](https://learn.microsoft.com/en-us/entra/identity-platform/reference-error-codes#aadsts-error-codes) — "The client secret provided is invalid" (misleading when the real issue is a client ID/secret mismatch) diff --git a/dotnet/work-iq-teams-bot/work-iq-teams-bot.TeamsApp/WorkIQAgent.ServiceExtensions.cs b/dotnet/work-iq-teams-bot/work-iq-teams-bot.TeamsApp/WorkIQAgent.ServiceExtensions.cs index 3c7a9992..f1f0495a 100644 --- a/dotnet/work-iq-teams-bot/work-iq-teams-bot.TeamsApp/WorkIQAgent.ServiceExtensions.cs +++ b/dotnet/work-iq-teams-bot/work-iq-teams-bot.TeamsApp/WorkIQAgent.ServiceExtensions.cs @@ -3,6 +3,7 @@ using Azure.AI.OpenAI; using Microsoft.Extensions.AI; +using Microsoft.Identity.Web; using Microsoft.Identity.Web.TokenCacheProviders; using Microsoft.Identity.Web.TokenCacheProviders.Distributed; using Microsoft.OpenTelemetry; @@ -28,6 +29,11 @@ public static IServiceCollection AddWorkIQAgent(this IServiceCollection services { services.Configure(configuration.GetSection(WorkIQAgentOptions.SectionName)); + // Register the Agent Identities MSAL add-in so that WithAgentUserIdentity() + // triggers the FIC (Federated Identity Credential) grant instead of falling + // back to a silent token acquisition with mismatched client credentials. + services.AddAgentIdentities(); + services.AddChatClient(sp => { IConfiguration config = sp.GetRequiredService(); @@ -42,7 +48,7 @@ public static IServiceCollection AddWorkIQAgent(this IServiceCollection services .UseFunctionInvocation() .UseOpenTelemetry(sourceName: "Experimental.Microsoft.Extensions.AI"); - services.AddSingleton(); + services.AddScoped(); // Conversation history must outlive any single turn -> singleton. services.AddSingleton(); diff --git a/dotnet/work-iq-teams-bot/work-iq-teams-bot.TeamsApp/WorkIQTeamsBotApp.cs b/dotnet/work-iq-teams-bot/work-iq-teams-bot.TeamsApp/WorkIQTeamsBotApp.cs index d4b6c6ad..d1776bb4 100644 --- a/dotnet/work-iq-teams-bot/work-iq-teams-bot.TeamsApp/WorkIQTeamsBotApp.cs +++ b/dotnet/work-iq-teams-bot/work-iq-teams-bot.TeamsApp/WorkIQTeamsBotApp.cs @@ -18,7 +18,7 @@ namespace work_iq_teams_bot.TeamsApp; /// internal sealed class WorkIQTeamsBotApp : TeamsBotApplication { - private readonly IServiceScopeFactory _scopeFactory; + private readonly IHttpContextAccessor _httpContextAccessor; public WorkIQTeamsBotApp( ConversationClient conversationClient, @@ -26,12 +26,11 @@ public WorkIQTeamsBotApp( ApiClient teamsApiClient, IHttpContextAccessor httpContextAccessor, ILogger logger, - IServiceScopeFactory scopeFactory, BotApplicationOptions? options = null, TeamsBotApplicationOptions? teamsOptions = null) : base(conversationClient, userTokenClient, teamsApiClient, httpContextAccessor, logger, options, teamsOptions) { - _scopeFactory = scopeFactory; + _httpContextAccessor = httpContextAccessor; this.OnMessage(HandleMessageAsync); } @@ -40,12 +39,12 @@ private async Task HandleMessageAsync(Context context, Cancella { await context.SendTypingActivityAsync(cancellationToken); - string userText = context.Activity.TextWithoutMentions ?? string.Empty; - - // Resolve Agent from a fresh per-turn scope so scoped services have a well-defined lifetime - // independent of the singleton bot application and of any ambient HTTP request scope. - await using AsyncServiceScope scope = _scopeFactory.CreateAsyncScope(); - WorkIQAgent agent = scope.ServiceProvider.GetRequiredService(); + // Resolve the agent from the HTTP request scope so the OBO token assertion + // (validated earlier in the pipeline) is available to IAuthorizationHeaderProvider. + // A detached scope (IServiceScopeFactory) would lose the HttpContext, causing + // MSAL's OBO flow to fail on the first turn before any tokens are cached. + IServiceProvider requestServices = _httpContextAccessor.HttpContext!.RequestServices; + WorkIQAgent agent = requestServices.GetRequiredService(); string response = await agent.RunAsync( context.Activity, From be32ae17c989d52e56bfcdd75e41c5c10d31cb55 Mon Sep 17 00:00:00 2001 From: "Ricardo Minguez Pablos (RIDO)" Date: Tue, 19 May 2026 08:36:58 -0700 Subject: [PATCH 3/9] Refactor and enhance OpenTelemetry integration Centralize OpenTelemetry setup in Extensions.cs with support for configurable activity sources and meters. Add resource attributes and enable both OTLP and Azure Monitor exporters. Implement HTTP client instrumentation filtering to suppress noisy MCP service spans. Remove redundant OpenTelemetry code from WorkIQAgent.ServiceExtensions.cs. Update .csproj dependencies accordingly. Add mcp-service-http-errors.md to document known MCP HTTP errors and trace filtering rationale. --- .../docs/mcp-service-http-errors.md | 69 ++++++++++++++ .../Extensions.cs | 89 +++++++++++-------- .../work-iq-teams-bot.ServiceDefaults.csproj | 2 + .../work-iq-teams-bot.TeamsApp/Program.cs | 8 +- .../WorkIQAgent.ServiceExtensions.cs | 23 ----- .../work-iq-teams-bot.TeamsApp.csproj | 5 -- 6 files changed, 131 insertions(+), 65 deletions(-) create mode 100644 dotnet/work-iq-teams-bot/docs/mcp-service-http-errors.md diff --git a/dotnet/work-iq-teams-bot/docs/mcp-service-http-errors.md b/dotnet/work-iq-teams-bot/docs/mcp-service-http-errors.md new file mode 100644 index 00000000..64b7fe14 --- /dev/null +++ b/dotnet/work-iq-teams-bot/docs/mcp-service-http-errors.md @@ -0,0 +1,69 @@ +# Agent365 MCP Service — HTTP Error Report + +**Date:** 2026-05-19 +**Reporter:** Work IQ Teams Bot sample +**Trace URL:** `https://localhost:17248/traces/detail/0f80ae6e014dca157f355a78d1fa244f` + +## Summary + +Two MCP server endpoints return unexpected HTTP errors during normal +Streamable HTTP transport lifecycle operations. The errors are raised by +the server, not the client SDK. + +## Issue 1 — GET returns 405 Method Not Allowed + +| Field | Value | +|-------|-------| +| **Endpoint** | `GET https://agent365.svc.cloud.microsoft/agents/servers/mcp_MailTools` | +| **Status** | `405 Method Not Allowed` | +| **Phase** | SSE listener setup (post-initialize) | + +### Details + +The MCP C# SDK (`ModelContextProtocol.Client`) sends a `GET` request to +open a Server-Sent Events stream for server-initiated notifications after +the `initialize` handshake completes. The `mcp_MailTools` server rejects +this with **405**. + +Per the [MCP Streamable HTTP specification](https://modelcontextprotocol.io/specification/2025-03-26/basic/transports#streamable-http), +`GET` is optional — servers that do not support server-initiated messages +may omit it. However, the expected response for an unsupported method is +still a well-formed HTTP error; the 405 is technically correct but creates +noise in traces because the client SDK does not suppress it. + +### Suggested fix + +Either: +- Return **501 Not Implemented** (clearer intent) and/or include an + `Allow: POST` header so the SDK can avoid retries, or +- Support the `GET` SSE endpoint (no-op keep-alive stream is fine). + +## Issue 2 — DELETE returns 500 Internal Server Error + +| Field | Value | +|-------|-------| +| **Endpoint** | `DELETE https://agent365.svc.cloud.microsoft/agents/servers/mcp_TeamsServer` | +| **Status** | `500 Internal Server Error` | +| **Phase** | Session teardown (`McpClient.DisposeAsync`) | + +### Details + +When the MCP client disposes, it sends a `DELETE` to terminate the session +as required by the Streamable HTTP transport. The `mcp_TeamsServer` returns +a **500** instead of the expected **200 / 204**. + +This is a server-side bug. The client already swallows disposal exceptions +so it does not affect end-user functionality, but it produces a failed span +on every conversation turn. + +### Suggested fix + +Fix the `DELETE` handler in `mcp_TeamsServer` to return **204 No Content** +on successful session teardown, or **404** if the session has already +expired. + +## Client-side mitigation + +An OpenTelemetry trace filter has been added to the sample to suppress +these known-noisy spans so they do not clutter Aspire dashboards while the +server-side fixes are pending. See `WorkIQAgent.ServiceExtensions.cs`. diff --git a/dotnet/work-iq-teams-bot/work-iq-teams-bot.ServiceDefaults/Extensions.cs b/dotnet/work-iq-teams-bot/work-iq-teams-bot.ServiceDefaults/Extensions.cs index d6821fe9..0e93e3f4 100644 --- a/dotnet/work-iq-teams-bot/work-iq-teams-bot.ServiceDefaults/Extensions.cs +++ b/dotnet/work-iq-teams-bot/work-iq-teams-bot.ServiceDefaults/Extensions.cs @@ -7,8 +7,10 @@ using Microsoft.Extensions.Diagnostics.HealthChecks; using Microsoft.Extensions.Logging; using Microsoft.Extensions.ServiceDiscovery; +using Microsoft.OpenTelemetry; using OpenTelemetry; using OpenTelemetry.Metrics; +using OpenTelemetry.Resources; using OpenTelemetry.Trace; namespace Microsoft.Extensions.Hosting; @@ -21,9 +23,12 @@ public static class Extensions private const string HealthEndpointPath = "/health"; private const string AlivenessEndpointPath = "/alive"; - public static TBuilder AddServiceDefaults(this TBuilder builder) where TBuilder : IHostApplicationBuilder + public static TBuilder AddServiceDefaults( + this TBuilder builder, + string[]? activitySources = null, + string[]? meterNames = null) where TBuilder : IHostApplicationBuilder { - builder.ConfigureOpenTelemetry(); + builder.ConfigureOpenTelemetry(activitySources, meterNames); builder.AddDefaultHealthChecks(); @@ -38,16 +43,13 @@ public static TBuilder AddServiceDefaults(this TBuilder builder) where http.AddServiceDiscovery(); }); - // Uncomment the following to restrict the allowed schemes for service discovery. - // builder.Services.Configure(options => - // { - // options.AllowedSchemes = ["https"]; - // }); - return builder; } - public static TBuilder ConfigureOpenTelemetry(this TBuilder builder) where TBuilder : IHostApplicationBuilder + public static TBuilder ConfigureOpenTelemetry( + this TBuilder builder, + string[]? activitySources = null, + string[]? meterNames = null) where TBuilder : IHostApplicationBuilder { builder.Logging.AddOpenTelemetry(logging => { @@ -56,47 +58,64 @@ public static TBuilder ConfigureOpenTelemetry(this TBuilder builder) w }); builder.Services.AddOpenTelemetry() + .ConfigureResource(r => r + .AddService( + serviceName: builder.Environment.ApplicationName, + serviceVersion: "0.0.1") + .AddAttributes(new Dictionary + { + ["service.namespace"] = "TeamsSamples" + })) + .UseMicrosoftOpenTelemetry(o => + { + o.Exporters = ExportTarget.Otlp | ExportTarget.AzureMonitor; + o.Instrumentation.EnableHttpClientInstrumentation = true; + o.Instrumentation.EnableAspNetCoreInstrumentation = true; + }) .WithMetrics(metrics => { metrics.AddAspNetCoreInstrumentation() .AddHttpClientInstrumentation() .AddRuntimeInstrumentation(); + + if (meterNames is { Length: > 0 }) + { + metrics.AddMeter(meterNames); + } }) .WithTracing(tracing => { tracing.AddSource(builder.Environment.ApplicationName) - .AddAspNetCoreInstrumentation(tracing => + .AddAspNetCoreInstrumentation(options => // Exclude health check requests from tracing - tracing.Filter = context => + options.Filter = context => !context.Request.Path.StartsWithSegments(HealthEndpointPath) && !context.Request.Path.StartsWithSegments(AlivenessEndpointPath) ) - // Uncomment the following line to enable gRPC instrumentation (requires the OpenTelemetry.Instrumentation.GrpcNetClient package) - //.AddGrpcClientInstrumentation() - .AddHttpClientInstrumentation(); + .AddHttpClientInstrumentation(options => + { + // Suppress known-noisy spans from Agent365 MCP service endpoints. + // GET (SSE listener) returns 405 and DELETE (session teardown) returns 500 + // due to server-side issues. See docs/mcp-service-http-errors.md. + options.FilterHttpRequestMessage = request => + { + if (request.RequestUri?.Host is "agent365.svc.cloud.microsoft" + && request.RequestUri.AbsolutePath.StartsWith("/agents/servers/", StringComparison.OrdinalIgnoreCase)) + { + return request.Method != HttpMethod.Get + && request.Method != HttpMethod.Delete; + } + + return true; + }; + }); + + if (activitySources is { Length: > 0 }) + { + tracing.AddSource(activitySources); + } }); - //builder.AddOpenTelemetryExporters(); - - return builder; - } - - private static TBuilder AddOpenTelemetryExporters(this TBuilder builder) where TBuilder : IHostApplicationBuilder - { - var useOtlpExporter = !string.IsNullOrWhiteSpace(builder.Configuration["OTEL_EXPORTER_OTLP_ENDPOINT"]); - - if (useOtlpExporter) - { - builder.Services.AddOpenTelemetry().UseOtlpExporter(); - } - - // Uncomment the following lines to enable the Azure Monitor exporter (requires the Azure.Monitor.OpenTelemetry.AspNetCore package) - //if (!string.IsNullOrEmpty(builder.Configuration["APPLICATIONINSIGHTS_CONNECTION_STRING"])) - //{ - // builder.Services.AddOpenTelemetry() - // .UseAzureMonitor(); - //} - return builder; } diff --git a/dotnet/work-iq-teams-bot/work-iq-teams-bot.ServiceDefaults/work-iq-teams-bot.ServiceDefaults.csproj b/dotnet/work-iq-teams-bot/work-iq-teams-bot.ServiceDefaults/work-iq-teams-bot.ServiceDefaults.csproj index 791c654e..3075219b 100644 --- a/dotnet/work-iq-teams-bot/work-iq-teams-bot.ServiceDefaults/work-iq-teams-bot.ServiceDefaults.csproj +++ b/dotnet/work-iq-teams-bot/work-iq-teams-bot.ServiceDefaults/work-iq-teams-bot.ServiceDefaults.csproj @@ -12,6 +12,8 @@ + + diff --git a/dotnet/work-iq-teams-bot/work-iq-teams-bot.TeamsApp/Program.cs b/dotnet/work-iq-teams-bot/work-iq-teams-bot.TeamsApp/Program.cs index a9e01fa3..5d44252f 100644 --- a/dotnet/work-iq-teams-bot/work-iq-teams-bot.TeamsApp/Program.cs +++ b/dotnet/work-iq-teams-bot/work-iq-teams-bot.TeamsApp/Program.cs @@ -1,12 +1,16 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -using work_iq_teams_bot.TeamsApp; using Microsoft.Teams.Apps; +using Microsoft.Teams.Apps.Diagnostics; +using Microsoft.Teams.Core.Diagnostics; +using work_iq_teams_bot.TeamsApp; WebApplicationBuilder builder = WebApplication.CreateSlimBuilder(args); -builder.AddServiceDefaults(); +builder.AddServiceDefaults( + activitySources: [CoreTelemetryNames.ActivitySourceName, TeamsBotApplicationTelemetry.ActivitySourceName, "Experimental.Microsoft.Agents.AI", "ModelContextProtocol"], + meterNames: [CoreTelemetryNames.MeterName, TeamsBotApplicationTelemetry.MeterName, "Experimental.Microsoft.Agents.AI", "ModelContextProtocol"]); builder.Services .AddWorkIQAgent(builder.Configuration) diff --git a/dotnet/work-iq-teams-bot/work-iq-teams-bot.TeamsApp/WorkIQAgent.ServiceExtensions.cs b/dotnet/work-iq-teams-bot/work-iq-teams-bot.TeamsApp/WorkIQAgent.ServiceExtensions.cs index f1f0495a..8b425329 100644 --- a/dotnet/work-iq-teams-bot/work-iq-teams-bot.TeamsApp/WorkIQAgent.ServiceExtensions.cs +++ b/dotnet/work-iq-teams-bot/work-iq-teams-bot.TeamsApp/WorkIQAgent.ServiceExtensions.cs @@ -6,11 +6,6 @@ using Microsoft.Identity.Web; using Microsoft.Identity.Web.TokenCacheProviders; using Microsoft.Identity.Web.TokenCacheProviders.Distributed; -using Microsoft.OpenTelemetry; -using Microsoft.Teams.Apps.Diagnostics; -using Microsoft.Teams.Core.Diagnostics; -using OpenTelemetry; -using OpenTelemetry.Resources; using System.ClientModel; namespace work_iq_teams_bot.TeamsApp; @@ -61,24 +56,6 @@ public static IServiceCollection AddWorkIQAgent(this IServiceCollection services services.AddDistributedMemoryCache(); services.AddSingleton(); - string[] activitySources = [CoreTelemetryNames.ActivitySourceName, TeamsBotApplicationTelemetry.ActivitySourceName, "Experimental.Microsoft.Agents.AI", "ModelContextProtocol"]; - string[] meterNames = [CoreTelemetryNames.MeterName, TeamsBotApplicationTelemetry.MeterName, "Experimental.Microsoft.Agents.AI", "ModelContextProtocol"]; - - services.AddOpenTelemetry() - .ConfigureResource(r => r - .AddService(serviceName: "WorkIQTeamsBot", serviceVersion: "0.0.1") - .AddAttributes(new Dictionary - { - ["service.namespace"] = "TeamsSamples" - })) - .UseMicrosoftOpenTelemetry(o => - { - o.Exporters = ExportTarget.Otlp | ExportTarget.AzureMonitor; - o.Instrumentation.EnableHttpClientInstrumentation = true; - o.Instrumentation.EnableAspNetCoreInstrumentation = true; - }) - .WithTracing(t => t.AddSource(activitySources)) - .WithMetrics(m => m.AddMeter(meterNames)); return services; } } diff --git a/dotnet/work-iq-teams-bot/work-iq-teams-bot.TeamsApp/work-iq-teams-bot.TeamsApp.csproj b/dotnet/work-iq-teams-bot/work-iq-teams-bot.TeamsApp/work-iq-teams-bot.TeamsApp.csproj index dba42930..b5221931 100644 --- a/dotnet/work-iq-teams-bot/work-iq-teams-bot.TeamsApp/work-iq-teams-bot.TeamsApp.csproj +++ b/dotnet/work-iq-teams-bot/work-iq-teams-bot.TeamsApp/work-iq-teams-bot.TeamsApp.csproj @@ -18,11 +18,6 @@ - - - - - From 816439d3f1a0d6ef72b80ce9e4ca9663c39c4427 Mon Sep 17 00:00:00 2001 From: "Ricardo Minguez Pablos (RIDO)" Date: Tue, 19 May 2026 09:38:26 -0700 Subject: [PATCH 4/9] Downgrade Teams.Apps package and update telemetry config Commented out unused diagnostics usings in Program.cs. Modified AddServiceDefaults to only include experimental activity sources and meters. Downgraded Microsoft.Teams.Apps NuGet package to 2.1.0-preview-0002 in the project file. --- .../work-iq-teams-bot.TeamsApp/Program.cs | 12 ++++++++---- .../work-iq-teams-bot.TeamsApp.csproj | 2 +- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/dotnet/work-iq-teams-bot/work-iq-teams-bot.TeamsApp/Program.cs b/dotnet/work-iq-teams-bot/work-iq-teams-bot.TeamsApp/Program.cs index 5d44252f..35b09607 100644 --- a/dotnet/work-iq-teams-bot/work-iq-teams-bot.TeamsApp/Program.cs +++ b/dotnet/work-iq-teams-bot/work-iq-teams-bot.TeamsApp/Program.cs @@ -2,15 +2,19 @@ // Licensed under the MIT License. using Microsoft.Teams.Apps; -using Microsoft.Teams.Apps.Diagnostics; -using Microsoft.Teams.Core.Diagnostics; +//using Microsoft.Teams.Apps.Diagnostics; +//using Microsoft.Teams.Core.Diagnostics; using work_iq_teams_bot.TeamsApp; WebApplicationBuilder builder = WebApplication.CreateSlimBuilder(args); +//builder.AddServiceDefaults( +// activitySources: [CoreTelemetryNames.ActivitySourceName, TeamsBotApplicationTelemetry.ActivitySourceName, "Experimental.Microsoft.Agents.AI", "ModelContextProtocol"], +// meterNames: [CoreTelemetryNames.MeterName, TeamsBotApplicationTelemetry.MeterName, "Experimental.Microsoft.Agents.AI", "ModelContextProtocol"]); + builder.AddServiceDefaults( - activitySources: [CoreTelemetryNames.ActivitySourceName, TeamsBotApplicationTelemetry.ActivitySourceName, "Experimental.Microsoft.Agents.AI", "ModelContextProtocol"], - meterNames: [CoreTelemetryNames.MeterName, TeamsBotApplicationTelemetry.MeterName, "Experimental.Microsoft.Agents.AI", "ModelContextProtocol"]); + activitySources: [ "Experimental.Microsoft.Agents.AI", "ModelContextProtocol"], + meterNames: [ "Experimental.Microsoft.Agents.AI", "ModelContextProtocol"]); builder.Services .AddWorkIQAgent(builder.Configuration) diff --git a/dotnet/work-iq-teams-bot/work-iq-teams-bot.TeamsApp/work-iq-teams-bot.TeamsApp.csproj b/dotnet/work-iq-teams-bot/work-iq-teams-bot.TeamsApp/work-iq-teams-bot.TeamsApp.csproj index b5221931..6aa73884 100644 --- a/dotnet/work-iq-teams-bot/work-iq-teams-bot.TeamsApp/work-iq-teams-bot.TeamsApp.csproj +++ b/dotnet/work-iq-teams-bot/work-iq-teams-bot.TeamsApp/work-iq-teams-bot.TeamsApp.csproj @@ -11,7 +11,7 @@ - + From 4348868847cffd69711bd65e9e6b1f8801a8cbe3 Mon Sep 17 00:00:00 2001 From: "Ricardo Minguez Pablos (RIDO)" Date: Tue, 19 May 2026 09:44:37 -0700 Subject: [PATCH 5/9] del docs --- .../docs/agent-identities-fic-issue.md | 59 ---------------- .../docs/mcp-service-http-errors.md | 69 ------------------- 2 files changed, 128 deletions(-) delete mode 100644 dotnet/work-iq-teams-bot/docs/agent-identities-fic-issue.md delete mode 100644 dotnet/work-iq-teams-bot/docs/mcp-service-http-errors.md diff --git a/dotnet/work-iq-teams-bot/docs/agent-identities-fic-issue.md b/dotnet/work-iq-teams-bot/docs/agent-identities-fic-issue.md deleted file mode 100644 index fb479048..00000000 --- a/dotnet/work-iq-teams-bot/docs/agent-identities-fic-issue.md +++ /dev/null @@ -1,59 +0,0 @@ -# Missing `AddAgentIdentities()` Causes AADSTS7000215 on First Turn - -## Symptom - -When the bot receives its first message, all MCP client token acquisitions fail with: - -``` -ErrorCode: invalid_client -HTTP StatusCode 401 -Microsoft Entra ID Error Code AADSTS7000215 -``` - -MSAL logs show `Token Acquisition (1007) failed` — one failure per MCP server (4 servers = 4 failures, all at the same timestamp). Subsequent turns may succeed because MSAL caches tokens from a degraded fallback path. - -## Root Cause - -`WithAgentUserIdentity()` (from `Microsoft.Identity.Web.AgentIdentities`) configures `AuthorizationHeaderProviderOptions` with: - -- `ClientId` set to the **agent application ID** (e.g., `9218bdba-...`) -- `AgentIdentityKey` and `UserIdKey` in `ExtraParameters` - -These parameters are designed for the **FIC (Federated Identity Credential)** grant flow (`grant_type=user_fic`), which is a two-step process: - -1. Acquire a FIC token for the agent app using the **blueprint's client credentials** (from the `AzureAd` config section) -2. Exchange that for a user FIC assertion scoped to the agent identity - -The MSAL add-in that implements this flow (`AgentUserIdentityMsalAddIn.OnBeforeUserFicForAgentUserIdentityAsync`) is registered by calling `services.AddAgentIdentities()`. **If this call is missing**, MSAL never hooks the FIC handler and falls back to a standard silent token acquisition where: - -- `client_id` = agent application ID (`9218bdba-...`) — set by `WithAgentUserIdentity` -- `client_secret` = the blueprint's secret (from the `AzureAd` config, which has `ClientId = 74018ebb-...`) - -Entra ID rejects this because the secret does not belong to the agent application — it belongs to the blueprint. This produces `AADSTS7000215` (`invalid_client`). - -## Fix - -Register the Agent Identities MSAL add-in in your DI setup: - -```csharp -using Microsoft.Identity.Web; - -services.AddAgentIdentities(); -``` - -This registers the `OnBeforeTokenAcquisitionForTestUserAsync` callback that intercepts token requests with agent identity parameters and rewrites them into the correct FIC grant flow. - -## Diagnostic Checklist - -| Check | Expected | -|-------|----------| -| `AddAgentIdentities()` called in DI setup | Yes | -| `AzureAd` config has blueprint's `ClientId` + `ClientSecret` | Yes | -| `WithAgentUserIdentity()` receives the agent app ID + user OID | Yes | -| `Microsoft.Identity.Web.AgentIdentities` NuGet package referenced | Yes | - -## References - -- `Microsoft.Identity.Web.AgentIdentities` source: [`AgentIdentitiesExtension.cs`](https://github.com/AzureAD/microsoft-identity-web/blob/main/src/Microsoft.Identity.Web.AgentIdentities/AgentIdentitiesExtension.cs) -- FIC grant handler: [`AgentUserIdentityMsalAddIn.cs`](https://github.com/AzureAD/microsoft-identity-web/blob/main/src/Microsoft.Identity.Web.AgentIdentities/AgentUserIdentityMsalAddIn.cs) -- Entra error reference: [AADSTS7000215](https://learn.microsoft.com/en-us/entra/identity-platform/reference-error-codes#aadsts-error-codes) — "The client secret provided is invalid" (misleading when the real issue is a client ID/secret mismatch) diff --git a/dotnet/work-iq-teams-bot/docs/mcp-service-http-errors.md b/dotnet/work-iq-teams-bot/docs/mcp-service-http-errors.md deleted file mode 100644 index 64b7fe14..00000000 --- a/dotnet/work-iq-teams-bot/docs/mcp-service-http-errors.md +++ /dev/null @@ -1,69 +0,0 @@ -# Agent365 MCP Service — HTTP Error Report - -**Date:** 2026-05-19 -**Reporter:** Work IQ Teams Bot sample -**Trace URL:** `https://localhost:17248/traces/detail/0f80ae6e014dca157f355a78d1fa244f` - -## Summary - -Two MCP server endpoints return unexpected HTTP errors during normal -Streamable HTTP transport lifecycle operations. The errors are raised by -the server, not the client SDK. - -## Issue 1 — GET returns 405 Method Not Allowed - -| Field | Value | -|-------|-------| -| **Endpoint** | `GET https://agent365.svc.cloud.microsoft/agents/servers/mcp_MailTools` | -| **Status** | `405 Method Not Allowed` | -| **Phase** | SSE listener setup (post-initialize) | - -### Details - -The MCP C# SDK (`ModelContextProtocol.Client`) sends a `GET` request to -open a Server-Sent Events stream for server-initiated notifications after -the `initialize` handshake completes. The `mcp_MailTools` server rejects -this with **405**. - -Per the [MCP Streamable HTTP specification](https://modelcontextprotocol.io/specification/2025-03-26/basic/transports#streamable-http), -`GET` is optional — servers that do not support server-initiated messages -may omit it. However, the expected response for an unsupported method is -still a well-formed HTTP error; the 405 is technically correct but creates -noise in traces because the client SDK does not suppress it. - -### Suggested fix - -Either: -- Return **501 Not Implemented** (clearer intent) and/or include an - `Allow: POST` header so the SDK can avoid retries, or -- Support the `GET` SSE endpoint (no-op keep-alive stream is fine). - -## Issue 2 — DELETE returns 500 Internal Server Error - -| Field | Value | -|-------|-------| -| **Endpoint** | `DELETE https://agent365.svc.cloud.microsoft/agents/servers/mcp_TeamsServer` | -| **Status** | `500 Internal Server Error` | -| **Phase** | Session teardown (`McpClient.DisposeAsync`) | - -### Details - -When the MCP client disposes, it sends a `DELETE` to terminate the session -as required by the Streamable HTTP transport. The `mcp_TeamsServer` returns -a **500** instead of the expected **200 / 204**. - -This is a server-side bug. The client already swallows disposal exceptions -so it does not affect end-user functionality, but it produces a failed span -on every conversation turn. - -### Suggested fix - -Fix the `DELETE` handler in `mcp_TeamsServer` to return **204 No Content** -on successful session teardown, or **404** if the session has already -expired. - -## Client-side mitigation - -An OpenTelemetry trace filter has been added to the sample to suppress -these known-noisy spans so they do not clutter Aspire dashboards while the -server-side fixes are pending. See `WorkIQAgent.ServiceExtensions.cs`. From d136c488321b9ae1d47d5c18d0c449fa645fd52a Mon Sep 17 00:00:00 2001 From: "Ricardo Minguez Pablos (RIDO)" Date: Tue, 19 May 2026 13:34:44 -0700 Subject: [PATCH 6/9] configure exporter --- .../work-iq-teams-bot.AppHost/AppHost.cs | 2 +- .../Extensions.cs | 25 ++++++++++++++++--- .../work-iq-teams-bot.ServiceDefaults.csproj | 4 ++- .../work-iq-teams-bot.TeamsApp/Program.cs | 9 +++++-- 4 files changed, 32 insertions(+), 8 deletions(-) diff --git a/dotnet/work-iq-teams-bot/work-iq-teams-bot.AppHost/AppHost.cs b/dotnet/work-iq-teams-bot/work-iq-teams-bot.AppHost/AppHost.cs index 18cae944..fbfaa4a7 100644 --- a/dotnet/work-iq-teams-bot/work-iq-teams-bot.AppHost/AppHost.cs +++ b/dotnet/work-iq-teams-bot/work-iq-teams-bot.AppHost/AppHost.cs @@ -3,7 +3,7 @@ var builder = DistributedApplication.CreateBuilder(args); -builder.AddProject("teamsbotapp") +builder.AddProject("teamsbotapp", "Teams365Demo") .WithHttpHealthCheck("/health"); builder.Build().Run(); diff --git a/dotnet/work-iq-teams-bot/work-iq-teams-bot.ServiceDefaults/Extensions.cs b/dotnet/work-iq-teams-bot/work-iq-teams-bot.ServiceDefaults/Extensions.cs index 0e93e3f4..5f644274 100644 --- a/dotnet/work-iq-teams-bot/work-iq-teams-bot.ServiceDefaults/Extensions.cs +++ b/dotnet/work-iq-teams-bot/work-iq-teams-bot.ServiceDefaults/Extensions.cs @@ -7,6 +7,8 @@ using Microsoft.Extensions.Diagnostics.HealthChecks; using Microsoft.Extensions.Logging; using Microsoft.Extensions.ServiceDiscovery; +using Microsoft.Identity.Abstractions; +using Microsoft.Identity.Web; using Microsoft.OpenTelemetry; using OpenTelemetry; using OpenTelemetry.Metrics; @@ -26,9 +28,10 @@ public static class Extensions public static TBuilder AddServiceDefaults( this TBuilder builder, string[]? activitySources = null, - string[]? meterNames = null) where TBuilder : IHostApplicationBuilder + string[]? meterNames = null, + Func? rootProviderAccessor = null) where TBuilder : IHostApplicationBuilder { - builder.ConfigureOpenTelemetry(activitySources, meterNames); + builder.ConfigureOpenTelemetry(activitySources, meterNames, rootProviderAccessor); builder.AddDefaultHealthChecks(); @@ -49,7 +52,8 @@ public static TBuilder AddServiceDefaults( public static TBuilder ConfigureOpenTelemetry( this TBuilder builder, string[]? activitySources = null, - string[]? meterNames = null) where TBuilder : IHostApplicationBuilder + string[]? meterNames = null, + Func? rootProviderAccessor = null) where TBuilder : IHostApplicationBuilder { builder.Logging.AddOpenTelemetry(logging => { @@ -68,9 +72,22 @@ public static TBuilder ConfigureOpenTelemetry( })) .UseMicrosoftOpenTelemetry(o => { - o.Exporters = ExportTarget.Otlp | ExportTarget.AzureMonitor; + o.Exporters = ExportTarget.Otlp | ExportTarget.AzureMonitor | ExportTarget.Agent365; o.Instrumentation.EnableHttpClientInstrumentation = true; o.Instrumentation.EnableAspNetCoreInstrumentation = true; + o.Agent365.Exporter.UseS2SEndpoint = true; + if (rootProviderAccessor is not null) + { + o.Agent365.Exporter.TokenResolver = async (agentId, tenantId) => + { + var provider = rootProviderAccessor().GetRequiredService(); + var options = new AuthorizationHeaderProviderOptions { AcquireTokenOptions = new() { AuthenticationOptionsName = "AzureAd", Tenant = tenantId } }; + options.WithAgentIdentity(agentId); + var token = await provider.CreateAuthorizationHeaderForAppAsync( + "api://9b975845-388f-4429-889e-eab1ef63949c/.default", options); + return token.Substring("Bearer".Length).Trim(); + }; + } }) .WithMetrics(metrics => { diff --git a/dotnet/work-iq-teams-bot/work-iq-teams-bot.ServiceDefaults/work-iq-teams-bot.ServiceDefaults.csproj b/dotnet/work-iq-teams-bot/work-iq-teams-bot.ServiceDefaults/work-iq-teams-bot.ServiceDefaults.csproj index 3075219b..8d7a8ba5 100644 --- a/dotnet/work-iq-teams-bot/work-iq-teams-bot.ServiceDefaults/work-iq-teams-bot.ServiceDefaults.csproj +++ b/dotnet/work-iq-teams-bot/work-iq-teams-bot.ServiceDefaults/work-iq-teams-bot.ServiceDefaults.csproj @@ -1,4 +1,4 @@ - + net10.0 @@ -13,6 +13,8 @@ + + diff --git a/dotnet/work-iq-teams-bot/work-iq-teams-bot.TeamsApp/Program.cs b/dotnet/work-iq-teams-bot/work-iq-teams-bot.TeamsApp/Program.cs index 35b09607..59ca8ebe 100644 --- a/dotnet/work-iq-teams-bot/work-iq-teams-bot.TeamsApp/Program.cs +++ b/dotnet/work-iq-teams-bot/work-iq-teams-bot.TeamsApp/Program.cs @@ -6,7 +6,10 @@ //using Microsoft.Teams.Core.Diagnostics; using work_iq_teams_bot.TeamsApp; + + WebApplicationBuilder builder = WebApplication.CreateSlimBuilder(args); +IServiceProvider? rootServiceProvider = null; //builder.AddServiceDefaults( // activitySources: [CoreTelemetryNames.ActivitySourceName, TeamsBotApplicationTelemetry.ActivitySourceName, "Experimental.Microsoft.Agents.AI", "ModelContextProtocol"], @@ -14,14 +17,16 @@ builder.AddServiceDefaults( activitySources: [ "Experimental.Microsoft.Agents.AI", "ModelContextProtocol"], - meterNames: [ "Experimental.Microsoft.Agents.AI", "ModelContextProtocol"]); + meterNames: [ "Experimental.Microsoft.Agents.AI", "ModelContextProtocol"], + rootProviderAccessor: () => rootServiceProvider!); builder.Services .AddWorkIQAgent(builder.Configuration) .AddTeamsBotApplication(); WebApplication webApp = builder.Build(); - +rootServiceProvider = webApp.Services; webApp.MapDefaultEndpoints(); webApp.UseTeamsBotApplication(); webApp.Run(); + From a7a65ba3f89fa5e5b3b0151b947e221db142a86f Mon Sep 17 00:00:00 2001 From: "Ricardo Minguez Pablos (RIDO)" Date: Tue, 19 May 2026 15:22:04 -0700 Subject: [PATCH 7/9] add preview --- .../work-iq-teams-bot.TeamsApp/Program.cs | 14 ++++---------- .../work-iq-teams-bot.TeamsApp.csproj | 2 +- 2 files changed, 5 insertions(+), 11 deletions(-) diff --git a/dotnet/work-iq-teams-bot/work-iq-teams-bot.TeamsApp/Program.cs b/dotnet/work-iq-teams-bot/work-iq-teams-bot.TeamsApp/Program.cs index 59ca8ebe..d933745b 100644 --- a/dotnet/work-iq-teams-bot/work-iq-teams-bot.TeamsApp/Program.cs +++ b/dotnet/work-iq-teams-bot/work-iq-teams-bot.TeamsApp/Program.cs @@ -2,22 +2,16 @@ // Licensed under the MIT License. using Microsoft.Teams.Apps; -//using Microsoft.Teams.Apps.Diagnostics; -//using Microsoft.Teams.Core.Diagnostics; +using Microsoft.Teams.Apps.Diagnostics; +using Microsoft.Teams.Core.Diagnostics; using work_iq_teams_bot.TeamsApp; - - WebApplicationBuilder builder = WebApplication.CreateSlimBuilder(args); IServiceProvider? rootServiceProvider = null; -//builder.AddServiceDefaults( -// activitySources: [CoreTelemetryNames.ActivitySourceName, TeamsBotApplicationTelemetry.ActivitySourceName, "Experimental.Microsoft.Agents.AI", "ModelContextProtocol"], -// meterNames: [CoreTelemetryNames.MeterName, TeamsBotApplicationTelemetry.MeterName, "Experimental.Microsoft.Agents.AI", "ModelContextProtocol"]); - builder.AddServiceDefaults( - activitySources: [ "Experimental.Microsoft.Agents.AI", "ModelContextProtocol"], - meterNames: [ "Experimental.Microsoft.Agents.AI", "ModelContextProtocol"], + activitySources: [CoreTelemetryNames.ActivitySourceName, TeamsBotApplicationTelemetry.ActivitySourceName, "Experimental.Microsoft.Agents.AI", "ModelContextProtocol"], + meterNames: [CoreTelemetryNames.MeterName, TeamsBotApplicationTelemetry.MeterName, "Experimental.Microsoft.Agents.AI", "ModelContextProtocol"], rootProviderAccessor: () => rootServiceProvider!); builder.Services diff --git a/dotnet/work-iq-teams-bot/work-iq-teams-bot.TeamsApp/work-iq-teams-bot.TeamsApp.csproj b/dotnet/work-iq-teams-bot/work-iq-teams-bot.TeamsApp/work-iq-teams-bot.TeamsApp.csproj index 6aa73884..b5221931 100644 --- a/dotnet/work-iq-teams-bot/work-iq-teams-bot.TeamsApp/work-iq-teams-bot.TeamsApp.csproj +++ b/dotnet/work-iq-teams-bot/work-iq-teams-bot.TeamsApp/work-iq-teams-bot.TeamsApp.csproj @@ -11,7 +11,7 @@ - + From 6f35529183da04fbb216b84c6eb494b1ffd7a46b Mon Sep 17 00:00:00 2001 From: "Ricardo Minguez Pablos (RIDO)" Date: Wed, 20 May 2026 06:58:13 -0700 Subject: [PATCH 8/9] simplify deps --- .../work-iq-teams-bot.TeamsApp.csproj | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/dotnet/work-iq-teams-bot/work-iq-teams-bot.TeamsApp/work-iq-teams-bot.TeamsApp.csproj b/dotnet/work-iq-teams-bot/work-iq-teams-bot.TeamsApp/work-iq-teams-bot.TeamsApp.csproj index b5221931..10e239b1 100644 --- a/dotnet/work-iq-teams-bot/work-iq-teams-bot.TeamsApp/work-iq-teams-bot.TeamsApp.csproj +++ b/dotnet/work-iq-teams-bot/work-iq-teams-bot.TeamsApp/work-iq-teams-bot.TeamsApp.csproj @@ -1,4 +1,4 @@ - + net10.0 @@ -11,13 +11,11 @@ + - - - From 55e3db9ba5ea031bfde6eb30c418e25432514d14 Mon Sep 17 00:00:00 2001 From: "Ricardo Minguez Pablos (RIDO)" Date: Wed, 20 May 2026 06:58:43 -0700 Subject: [PATCH 9/9] reorder csproj --- .../work-iq-teams-bot.TeamsApp.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dotnet/work-iq-teams-bot/work-iq-teams-bot.TeamsApp/work-iq-teams-bot.TeamsApp.csproj b/dotnet/work-iq-teams-bot/work-iq-teams-bot.TeamsApp/work-iq-teams-bot.TeamsApp.csproj index 10e239b1..4cbf5933 100644 --- a/dotnet/work-iq-teams-bot/work-iq-teams-bot.TeamsApp/work-iq-teams-bot.TeamsApp.csproj +++ b/dotnet/work-iq-teams-bot/work-iq-teams-bot.TeamsApp/work-iq-teams-bot.TeamsApp.csproj @@ -11,8 +11,8 @@ - +