diff --git a/.gitignore b/.gitignore
index def52263..7c590995 100644
--- a/.gitignore
+++ b/.gitignore
@@ -491,3 +491,7 @@ $RECYCLE.BIN/
# Claude local settings
.claude/settings.local.json
+
+# Claude session and private files
+session-context.md
+*-private.md
diff --git a/CLAUDE.md b/CLAUDE.md
new file mode 100644
index 00000000..06a84e8f
--- /dev/null
+++ b/CLAUDE.md
@@ -0,0 +1,178 @@
+# CLAUDE.md
+
+This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
+
+## Startup
+
+Before responding to the user's first message, complete these steps:
+
+### 1. Read knowledge files
+- Read `Claude-KB.md` in this directory (domain knowledge, lessons learned). Create it if it doesn't exist with a `## Lessons Learned` heading.
+- **Don't read md files from the parent directory unless the user requests it** — this slows down session start.
+- Look for a `*-private.md` file matching the user's name (e.g., `Rajan-private.md`). If one exists, read it — it contains personal TODOs, preferences, and reminders. These files are gitignored and never committed.
+- The private file may reference a durable location (e.g., a private git repo). If it does, also read and update that location for persistent notes and TODOs.
+
+### 2. Read session context
+- Read `session-context.md` if it exists. It contains ephemeral state from the previous session: what was in flight, what to pick up, any "don't forget" items. This file is gitignored and overwritten each save.
+- Surface relevant items in the greeting (e.g., "Last session you were working on PR 1234").
+
+### 3. Greet the user and surface
+- Any open TODOs or reminders from private notes
+- Common scenarios / quick-start commands:
+ - **Build the solution** — `dotnet build`
+ - **Run all tests** — `dotnet test`
+ - **Run a specific test** — `dotnet test --filter "FullyQualifiedName~TestName"`
+ - **Run a sample app** — `dotnet run --project Samples/Samples.Echo`
+ - **Format code** — `dotnet format`
+ - **Create NuGet packages** — `dotnet pack`
+
+## Build Commands
+
+```bash
+dotnet build # Build solution
+dotnet test # Run all tests
+dotnet test -v d # Run tests with detailed verbosity
+dotnet format # Format code (EditorConfig enforced)
+dotnet pack # Create NuGet packages
+```
+
+Run a specific test project:
+```bash
+dotnet test Tests/Microsoft.Teams.Apps.Tests
+```
+
+Run a single test by name:
+```bash
+dotnet test --filter "FullyQualifiedName~TestMethodName"
+```
+
+Run tests with coverage:
+```bash
+dotnet test --collect:"XPlat Code Coverage"
+```
+
+Run a specific sample:
+```bash
+dotnet run --project Samples/Samples.Echo
+dotnet run --project Samples/Samples.Lights
+```
+
+## Development Workflow
+
+- Cannot push directly to main - all changes require a pull request
+- Create a feature branch, make changes, then open a PR
+- CI runs build, test, and lint checks on PRs
+
+## Architecture Overview
+
+This is the Microsoft Teams SDK for .NET (`Microsoft.Teams.sln`) - a suite of packages for building Teams bots and apps.
+
+### Core Libraries (Libraries/)
+
+- **Microsoft.Teams.Apps** - Core bot functionality: activity handling, message processing, routing, context management
+- **Microsoft.Teams.AI** - AI/LLM integration: chat plugins, function definitions, prompt templates
+- **Microsoft.Teams.AI.Models.OpenAI** - OpenAI-specific model implementation
+- **Microsoft.Teams.Api** - Teams API client for bot-to-Teams communication
+- **Microsoft.Teams.Cards** - Adaptive Cards support
+- **Microsoft.Teams.Common** - Shared utilities, JSON helpers, HTTP, logging, storage patterns
+
+### Extensions (Libraries/Microsoft.Teams.Extensions/)
+
+- **Configuration** - Configuration helpers
+- **Hosting** - ASP.NET Core DI integration
+- **Logging** - Microsoft.Extensions.Logging integration
+- **Graph** - Microsoft Graph integration
+
+### Plugins (Libraries/Microsoft.Teams.Plugins/)
+
+- **AspNetCore** - Core middleware for ASP.NET Core
+- **AspNetCore.DevTools** - Development tools
+- **AspNetCore.BotBuilder** - Bot Builder SDK adapter
+- **External.Mcp** / **External.McpClient** - Model Context Protocol integration
+
+## Code Patterns
+
+### Basic App Setup
+
+```csharp
+var builder = WebApplication.CreateBuilder(args);
+builder.AddTeams(); // Register Teams services
+var app = builder.Build();
+var teams = app.UseTeams(); // Get Teams middleware
+
+teams.OnMessage(async context => { // Handle messages
+ await context.Send("Hello!");
+});
+
+app.Run();
+```
+
+### AI Plugin
+
+```csharp
+[Prompt]
+[Prompt.Description("description")]
+[Prompt.Instructions("system instructions")]
+public class MyPrompt(IContext.Accessor accessor)
+{
+ [Function]
+ [Function.Description("what this function does")]
+ public string MyFunction([Param("param description")] string input)
+ {
+ return "result";
+ }
+}
+```
+
+## Code Style
+
+EditorConfig is strictly enforced. Key conventions:
+
+- **Namespaces**: File-scoped (`namespace Foo;`)
+- **Fields**: `_camelCase` for private, `s_camelCase` for private static
+- **Nullable**: Enabled throughout
+- **Async**: All async methods, CancellationToken support
+
+All files require Microsoft copyright header:
+```csharp
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+```
+
+## Testing
+
+- xUnit with Moq for mocking, implicit `using Xunit` in test projects
+- Test projects target net9.0 (libraries target net8.0)
+- Test naming: `{LibraryName}.Tests` in `Tests/` directory
+- Use `Microsoft.Teams.Apps.Testing` for test utilities
+
+## Lessons Learned
+
+This workspace is a **learning system**. Claude-KB.md contains a `## Lessons Learned` section that persists knowledge across sessions.
+
+### When to add an entry
+
+Proactively add a lesson whenever you encounter:
+
+- **Unexpected behavior** — an API, tool, or workflow didn't work as expected and you found the cause
+- **Workarounds** — a problem required a non-obvious solution that future sessions should know about
+- **User preferences** — the user corrects your approach or states a preference
+- **Process discoveries** — you learn how something actually works vs. how it's documented
+- **Pitfalls** — something that wasted time and could be avoided next time
+
+### How to add an entry
+
+Append to the `## Lessons Learned` section in `Claude-KB.md` using this format:
+
+```markdown
+### YYYY-MM-DD: Short descriptive title
+Description of what happened and what to do differently. Keep it concise and actionable.
+```
+
+### Guidelines
+
+- Write for your future self — assume no prior context from this session
+- Be specific: include tool names, flag names, error messages, or exact steps
+- Don't duplicate existing entries — read the section first
+- One entry per distinct lesson; don't bundle unrelated things
+- Ask the user before adding if you're unsure whether something qualifies
diff --git a/Claude-KB.md b/Claude-KB.md
new file mode 100644
index 00000000..1db80c16
--- /dev/null
+++ b/Claude-KB.md
@@ -0,0 +1,3 @@
+# Claude Knowledge Base — Microsoft Teams SDK for .NET
+
+## Lessons Learned
diff --git a/Libraries/Microsoft.Teams.Api/Auth/ClientCredentials.cs b/Libraries/Microsoft.Teams.Api/Auth/ClientCredentials.cs
index 7ae6d974..07d90593 100644
--- a/Libraries/Microsoft.Teams.Api/Auth/ClientCredentials.cs
+++ b/Libraries/Microsoft.Teams.Api/Auth/ClientCredentials.cs
@@ -11,6 +11,12 @@ public class ClientCredentials : IHttpCredentials
public string ClientSecret { get; set; }
public string? TenantId { get; set; }
+ ///
+ /// The Entra ID login endpoint, following the Microsoft Identity Web configuration schema.
+ /// Override this for sovereign clouds (e.g. "https://login.microsoftonline.us" for US Gov).
+ ///
+ public string Instance { get; set; } = "https://login.microsoftonline.com";
+
public ClientCredentials(string clientId, string clientSecret)
{
ClientId = clientId;
@@ -27,8 +33,9 @@ public ClientCredentials(string clientId, string clientSecret, string? tenantId)
public async Task Resolve(IHttpClient client, string[] scopes, CancellationToken cancellationToken = default)
{
var tenantId = TenantId ?? "botframework.com";
+ var instance = Instance.TrimEnd('/');
var request = HttpRequest.Post(
- $"https://login.microsoftonline.com/{tenantId}/oauth2/v2.0/token"
+ $"{instance}/{tenantId}/oauth2/v2.0/token"
);
request.Headers.Add("Content-Type", ["application/x-www-form-urlencoded"]);
diff --git a/Libraries/Microsoft.Teams.Extensions/Microsoft.Teams.Extensions.Configuration/Microsoft.Teams.Apps.Extensions/TeamsSettings.cs b/Libraries/Microsoft.Teams.Extensions/Microsoft.Teams.Extensions.Configuration/Microsoft.Teams.Apps.Extensions/TeamsSettings.cs
index 11a4e532..f0b8bffc 100644
--- a/Libraries/Microsoft.Teams.Extensions/Microsoft.Teams.Extensions.Configuration/Microsoft.Teams.Apps.Extensions/TeamsSettings.cs
+++ b/Libraries/Microsoft.Teams.Extensions/Microsoft.Teams.Extensions.Configuration/Microsoft.Teams.Apps.Extensions/TeamsSettings.cs
@@ -11,6 +11,12 @@ public class TeamsSettings
public string? ClientSecret { get; set; }
public string? TenantId { get; set; }
+ ///
+ /// The Entra ID login endpoint, following the Microsoft Identity Web configuration schema.
+ /// Override this for sovereign clouds (e.g. "https://login.microsoftonline.us" for US Gov).
+ ///
+ public string? Instance { get; set; }
+
public bool Empty
{
get { return ClientId == "" || ClientSecret == ""; }
@@ -22,7 +28,14 @@ public AppOptions Apply(AppOptions? options = null)
if (ClientId is not null && ClientSecret is not null && !Empty)
{
- options.Credentials = new ClientCredentials(ClientId, ClientSecret, TenantId);
+ var credentials = new ClientCredentials(ClientId, ClientSecret, TenantId);
+
+ if (Instance is not null)
+ {
+ credentials.Instance = Instance;
+ }
+
+ options.Credentials = credentials;
}
return options;
diff --git a/Libraries/Microsoft.Teams.Extensions/Microsoft.Teams.Extensions.Hosting/Microsoft.Teams.Apps.Extensions/HostApplicationBuilder.cs b/Libraries/Microsoft.Teams.Extensions/Microsoft.Teams.Extensions.Hosting/Microsoft.Teams.Apps.Extensions/HostApplicationBuilder.cs
index 01f28920..666eec4a 100644
--- a/Libraries/Microsoft.Teams.Extensions/Microsoft.Teams.Extensions.Hosting/Microsoft.Teams.Apps.Extensions/HostApplicationBuilder.cs
+++ b/Libraries/Microsoft.Teams.Extensions/Microsoft.Teams.Extensions.Hosting/Microsoft.Teams.Apps.Extensions/HostApplicationBuilder.cs
@@ -34,11 +34,18 @@ public static IHostApplicationBuilder AddTeamsCore(this IHostApplicationBuilder
// client credentials
if (options.Credentials is null && settings.ClientId is not null && settings.ClientSecret is not null && !settings.Empty)
{
- options.Credentials = new ClientCredentials(
+ var credentials = new ClientCredentials(
settings.ClientId,
settings.ClientSecret,
settings.TenantId
);
+
+ if (settings.Instance is not null)
+ {
+ credentials.Instance = settings.Instance;
+ }
+
+ options.Credentials = credentials;
}
options.Logger ??= new ConsoleLogger(loggingSettings);
@@ -59,11 +66,18 @@ public static IHostApplicationBuilder AddTeamsCore(this IHostApplicationBuilder
// client credentials
if (settings.ClientId is not null && settings.ClientSecret is not null && !settings.Empty)
{
- appBuilder = appBuilder.AddCredentials(new ClientCredentials(
+ var credentials = new ClientCredentials(
settings.ClientId,
settings.ClientSecret,
settings.TenantId
- ));
+ );
+
+ if (settings.Instance is not null)
+ {
+ credentials.Instance = settings.Instance;
+ }
+
+ appBuilder = appBuilder.AddCredentials(credentials);
}
var app = appBuilder.Build();
diff --git a/Libraries/Microsoft.Teams.Plugins/Microsoft.Teams.Plugins.AspNetCore/Extensions/HostApplicationBuilder.cs b/Libraries/Microsoft.Teams.Plugins/Microsoft.Teams.Plugins.AspNetCore/Extensions/HostApplicationBuilder.cs
index 760d94da..81862751 100644
--- a/Libraries/Microsoft.Teams.Plugins/Microsoft.Teams.Plugins.AspNetCore/Extensions/HostApplicationBuilder.cs
+++ b/Libraries/Microsoft.Teams.Plugins/Microsoft.Teams.Plugins.AspNetCore/Extensions/HostApplicationBuilder.cs
@@ -121,6 +121,12 @@ public static IHostApplicationBuilder AddTeamsTokenAuthentication(this IHostAppl
var settings = builder.Configuration.GetTeams();
var teamsValidationSettings = new TeamsValidationSettings();
+
+ if (!string.IsNullOrEmpty(settings.Instance))
+ {
+ teamsValidationSettings.Instance = settings.Instance;
+ }
+
if (!string.IsNullOrEmpty(settings.ClientId))
{
teamsValidationSettings.AddDefaultAudiences(settings.ClientId);
diff --git a/Libraries/Microsoft.Teams.Plugins/Microsoft.Teams.Plugins.AspNetCore/Extensions/TeamsValidationSettings.cs b/Libraries/Microsoft.Teams.Plugins/Microsoft.Teams.Plugins.AspNetCore/Extensions/TeamsValidationSettings.cs
index 5443c928..551ef9ac 100644
--- a/Libraries/Microsoft.Teams.Plugins/Microsoft.Teams.Plugins.AspNetCore/Extensions/TeamsValidationSettings.cs
+++ b/Libraries/Microsoft.Teams.Plugins/Microsoft.Teams.Plugins.AspNetCore/Extensions/TeamsValidationSettings.cs
@@ -6,13 +6,14 @@ public class TeamsValidationSettings
public List Audiences = [];
public List Issuers = [
"https://api.botframework.com",
- "https://sts.windows.net/d6d49420-f39b-4df7-a1dc-d59a935871db/", // Emulator Auth v3.1, 1.0 token
- "https://login.microsoftonline.com/d6d49420-f39b-4df7-a1dc-d59a935871db/v2.0", // Emulator Auth v3.1, 2.0 token
- "https://sts.windows.net/f8cdef31-a31e-4b4a-93e4-5f571e91255a/", // Emulator Auth v3.2, 1.0 token
- "https://login.microsoftonline.com/f8cdef31-a31e-4b4a-93e4-5f571e91255a/v2.0", // Emulator Auth v3.2, 2.0 token
- "https://sts.windows.net/69e9b82d-4842-4902-8d1e-abc5b98a55e8/", // Copilot Auth v1.0 token
- "https://login.microsoftonline.com/69e9b82d-4842-4902-8d1e-abc5b98a55e8/v2.0", // Copilot Auth v2.0 token
- ];
+ "https://sts.windows.net/d6d49420-f39b-4df7-a1dc-d59a935871db/", // Emulator Auth v3.1, 1.0 token
+ "https://login.microsoftonline.com/d6d49420-f39b-4df7-a1dc-d59a935871db/v2.0", // Emulator Auth v3.1, 2.0 token
+ "https://sts.windows.net/f8cdef31-a31e-4b4a-93e4-5f571e91255a/", // Emulator Auth v3.2, 1.0 token
+ "https://login.microsoftonline.com/f8cdef31-a31e-4b4a-93e4-5f571e91255a/v2.0", // Emulator Auth v3.2, 2.0 token
+ "https://sts.windows.net/69e9b82d-4842-4902-8d1e-abc5b98a55e8/", // Copilot Auth v1.0 token
+ "https://login.microsoftonline.com/69e9b82d-4842-4902-8d1e-abc5b98a55e8/v2.0", // Copilot Auth v2.0 token
+ ];
+ public string Instance { get; set; } = "https://login.microsoftonline.com";
public void AddDefaultAudiences(string ClientId)
{
@@ -29,13 +30,15 @@ public IEnumerable GetValidIssuersForTenant(string? tenantId)
var validIssuers = new List();
if (!string.IsNullOrEmpty(tenantId))
{
- validIssuers.Add($"https://login.microsoftonline.com/{tenantId}/");
+ var instance = Instance.TrimEnd('/');
+ validIssuers.Add($"{instance}/{tenantId}/");
}
return validIssuers;
}
public string GetTenantSpecificOpenIdMetadataUrl(string? tenantId)
{
- return $"https://login.microsoftonline.com/{tenantId ?? "common"}/v2.0/.well-known/openid-configuration";
+ var instance = Instance.TrimEnd('/');
+ return $"{instance}/{tenantId ?? "common"}/v2.0/.well-known/openid-configuration";
}
}
\ No newline at end of file
diff --git a/Tests/Microsoft.Teams.Api.Tests/Auth/ClientCredentialsTests.cs b/Tests/Microsoft.Teams.Api.Tests/Auth/ClientCredentialsTests.cs
new file mode 100644
index 00000000..aaa4d618
--- /dev/null
+++ b/Tests/Microsoft.Teams.Api.Tests/Auth/ClientCredentialsTests.cs
@@ -0,0 +1,35 @@
+using Microsoft.Teams.Api.Auth;
+
+namespace Microsoft.Teams.Api.Tests.Auth;
+
+public class ClientCredentialsTests
+{
+ [Fact]
+ public void Instance_DefaultsToPublicCloud()
+ {
+ var credentials = new ClientCredentials("client-id", "client-secret");
+
+ Assert.Equal("https://login.microsoftonline.com", credentials.Instance);
+ }
+
+ [Fact]
+ public void Instance_CanBeOverridden()
+ {
+ var credentials = new ClientCredentials("client-id", "client-secret")
+ {
+ Instance = "https://login.microsoftonline.us"
+ };
+
+ Assert.Equal("https://login.microsoftonline.us", credentials.Instance);
+ }
+
+ [Fact]
+ public void Constructor_WithTenantId_SetsProperties()
+ {
+ var credentials = new ClientCredentials("client-id", "client-secret", "tenant-id");
+
+ Assert.Equal("client-id", credentials.ClientId);
+ Assert.Equal("client-secret", credentials.ClientSecret);
+ Assert.Equal("tenant-id", credentials.TenantId);
+ }
+}
diff --git a/Tests/Microsoft.Teams.Plugins.AspNetCore.Tests/Extensions/TeamsValidationSettingsTests.cs b/Tests/Microsoft.Teams.Plugins.AspNetCore.Tests/Extensions/TeamsValidationSettingsTests.cs
new file mode 100644
index 00000000..265d9840
--- /dev/null
+++ b/Tests/Microsoft.Teams.Plugins.AspNetCore.Tests/Extensions/TeamsValidationSettingsTests.cs
@@ -0,0 +1,89 @@
+using Microsoft.Teams.Plugins.AspNetCore.Extensions;
+
+namespace Microsoft.Teams.Plugins.AspNetCore.Tests.Extensions;
+
+public class TeamsValidationSettingsTests
+{
+ [Fact]
+ public void Instance_DefaultsToPublicCloud()
+ {
+ var settings = new TeamsValidationSettings();
+
+ Assert.Equal("https://login.microsoftonline.com", settings.Instance);
+ }
+
+ [Fact]
+ public void GetValidIssuersForTenant_UsesDefaultInstance()
+ {
+ var settings = new TeamsValidationSettings();
+ var issuers = settings.GetValidIssuersForTenant("test-tenant").ToList();
+
+ Assert.Single(issuers);
+ Assert.Equal("https://login.microsoftonline.com/test-tenant/", issuers[0]);
+ }
+
+ [Fact]
+ public void GetValidIssuersForTenant_UsesCustomInstance()
+ {
+ var settings = new TeamsValidationSettings
+ {
+ Instance = "https://login.microsoftonline.us"
+ };
+ var issuers = settings.GetValidIssuersForTenant("test-tenant").ToList();
+
+ Assert.Single(issuers);
+ Assert.Equal("https://login.microsoftonline.us/test-tenant/", issuers[0]);
+ }
+
+ [Fact]
+ public void GetValidIssuersForTenant_HandlesTrailingSlashInInstance()
+ {
+ var settings = new TeamsValidationSettings
+ {
+ Instance = "https://login.microsoftonline.us/"
+ };
+ var issuers = settings.GetValidIssuersForTenant("test-tenant").ToList();
+
+ Assert.Single(issuers);
+ Assert.Equal("https://login.microsoftonline.us/test-tenant/", issuers[0]);
+ }
+
+ [Fact]
+ public void GetTenantSpecificOpenIdMetadataUrl_UsesDefaultInstance()
+ {
+ var settings = new TeamsValidationSettings();
+ var url = settings.GetTenantSpecificOpenIdMetadataUrl("test-tenant");
+
+ Assert.Equal("https://login.microsoftonline.com/test-tenant/v2.0/.well-known/openid-configuration", url);
+ }
+
+ [Fact]
+ public void GetTenantSpecificOpenIdMetadataUrl_UsesCustomInstance()
+ {
+ var settings = new TeamsValidationSettings
+ {
+ Instance = "https://login.microsoftonline.us"
+ };
+ var url = settings.GetTenantSpecificOpenIdMetadataUrl("test-tenant");
+
+ Assert.Equal("https://login.microsoftonline.us/test-tenant/v2.0/.well-known/openid-configuration", url);
+ }
+
+ [Fact]
+ public void GetTenantSpecificOpenIdMetadataUrl_UsesCommon_WhenTenantIdIsNull()
+ {
+ var settings = new TeamsValidationSettings();
+ var url = settings.GetTenantSpecificOpenIdMetadataUrl(null);
+
+ Assert.Equal("https://login.microsoftonline.com/common/v2.0/.well-known/openid-configuration", url);
+ }
+
+ [Fact]
+ public void GetValidIssuersForTenant_ReturnsEmpty_WhenTenantIdIsNull()
+ {
+ var settings = new TeamsValidationSettings();
+ var issuers = settings.GetValidIssuersForTenant(null).ToList();
+
+ Assert.Empty(issuers);
+ }
+}