Skip to content
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -491,3 +491,7 @@ $RECYCLE.BIN/

# Claude local settings
.claude/settings.local.json

# Claude session and private files
session-context.md
*-private.md
178 changes: 178 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -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
3 changes: 3 additions & 0 deletions Claude-KB.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# Claude Knowledge Base — Microsoft Teams SDK for .NET

## Lessons Learned
9 changes: 8 additions & 1 deletion Libraries/Microsoft.Teams.Api/Auth/ClientCredentials.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,12 @@ public class ClientCredentials : IHttpCredentials
public string ClientSecret { get; set; }
public string? TenantId { get; set; }

/// <summary>
/// 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).
/// </summary>
public string Instance { get; set; } = "https://login.microsoftonline.com";

public ClientCredentials(string clientId, string clientSecret)
{
ClientId = clientId;
Expand All @@ -27,8 +33,9 @@ public ClientCredentials(string clientId, string clientSecret, string? tenantId)
public async Task<ITokenResponse> 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"]);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,12 @@ public class TeamsSettings
public string? ClientSecret { get; set; }
public string? TenantId { get; set; }

/// <summary>
/// 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).
/// </summary>
public string? Instance { get; set; }

public bool Empty
{
get { return ClientId == "" || ClientSecret == ""; }
Expand All @@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,14 @@ public class TeamsValidationSettings
public List<string> Audiences = [];
public List<string> 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)
{
Expand All @@ -29,13 +30,15 @@ public IEnumerable<string> GetValidIssuersForTenant(string? tenantId)
var validIssuers = new List<string>();
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";
}
}
35 changes: 35 additions & 0 deletions Tests/Microsoft.Teams.Api.Tests/Auth/ClientCredentialsTests.cs
Original file line number Diff line number Diff line change
@@ -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);
}
}
Loading