diff --git a/src/Runtime/Core/Utility.cs b/src/Runtime/Core/Utility.cs
index 7354d04e..cdf7455b 100644
--- a/src/Runtime/Core/Utility.cs
+++ b/src/Runtime/Core/Utility.cs
@@ -54,10 +54,18 @@ public static HttpClient GetDefaultHttpClient(IHttpClientFactory httpClientFacto
}
///
- /// Decodes the current token and retrieves the App ID (appid or azp claim).
+ /// WARNING: NO SIGNATURE VERIFICATION - This method uses JwtSecurityTokenHandler.ReadJwtToken()
+ /// which does NOT verify the token signature. The token claims can be spoofed by malicious actors.
+ /// This method is ONLY suitable for logging, analytics, and diagnostics purposes.
+ /// Do NOT use the returned value for authorization, access control, or security decisions.
+ /// Decodes the current token and retrieves the App ID (appid or azp claim).
+ /// Note: Returns a default GUID ('00000000-0000-0000-0000-000000000000') for empty tokens
+ /// for backward compatibility with callers that expect a valid-looking GUID.
+ /// For agent identification where empty string is preferred, use .
///
/// Token to Decode
- /// AppId
+ /// AppId, or default GUID for empty token
+ /// Thrown when token format is invalid
public static string GetAppIdFromToken(string token)
{
if (string.IsNullOrWhiteSpace(token))
@@ -70,6 +78,62 @@ public static string GetAppIdFromToken(string token)
return appIdClaim?.Value ?? string.Empty;
}
+ ///
+ /// WARNING: NO SIGNATURE VERIFICATION - This method uses JwtSecurityTokenHandler.ReadJwtToken()
+ /// which does NOT verify the token signature. The token claims can be spoofed by malicious actors.
+ /// This method is ONLY suitable for logging, analytics, and diagnostics purposes.
+ /// Do NOT use the returned value for authorization, access control, or security decisions.
+ /// Decodes the token and retrieves the best available agent identifier.
+ /// Checks claims in priority order: xms_par_app_azp (agent blueprint ID) > appid > azp.
+ /// Note: Returns empty string for empty/missing tokens (unlike which
+ /// returns a default GUID). This allows callers to omit headers when no identifier is available.
+ ///
+ /// JWT token to decode
+ /// Agent ID (GUID) or empty string if not found or token is empty
+ public static string GetAgentIdFromToken(string token)
+ {
+ if (string.IsNullOrWhiteSpace(token))
+ {
+ return string.Empty;
+ }
+
+ try
+ {
+ var handler = new JwtSecurityTokenHandler();
+ var jwtToken = handler.ReadJwtToken(token);
+
+ // Priority: xms_par_app_azp (agent blueprint ID) > appid > azp
+ var blueprintClaim = jwtToken.Claims.FirstOrDefault(c => c.Type == "xms_par_app_azp");
+ if (!string.IsNullOrEmpty(blueprintClaim?.Value))
+ {
+ return blueprintClaim.Value;
+ }
+
+ var appIdClaim = jwtToken.Claims.FirstOrDefault(c => c.Type == "appid");
+ if (!string.IsNullOrEmpty(appIdClaim?.Value))
+ {
+ return appIdClaim.Value;
+ }
+
+ var azpClaim = jwtToken.Claims.FirstOrDefault(c => c.Type == "azp");
+ return azpClaim?.Value ?? string.Empty;
+ }
+ catch
+ {
+ // Silent error handling - return empty string on decode failure
+ return string.Empty;
+ }
+ }
+
+ ///
+ /// Gets the application name from the entry assembly.
+ ///
+ /// Application name or null if not available.
+ public static string? GetApplicationName()
+ {
+ return System.Reflection.Assembly.GetEntryAssembly()?.GetName()?.Name;
+ }
+
///
/// Resolves the agent identity from the turn context or auth token.
///
diff --git a/src/Tests/Runtime.Tests/UtilityTests.cs b/src/Tests/Runtime.Tests/UtilityTests.cs
index ef9cace0..a66e91d7 100644
--- a/src/Tests/Runtime.Tests/UtilityTests.cs
+++ b/src/Tests/Runtime.Tests/UtilityTests.cs
@@ -4,6 +4,9 @@
using Microsoft.Extensions.Configuration;
using Microsoft.Agents.A365.Runtime.Utils;
using Moq;
+using System.IdentityModel.Tokens.Jwt;
+using System.Security.Claims;
+using System.Text;
namespace Microsoft.Agents.A365.Runtime.Tests
{
@@ -84,5 +87,189 @@ public void GetCurrentEnvironment_ReturnsDevelopment_WhenConfigMissing()
// Assert
Assert.Equal("Development", result);
}
+
+ #region GetAgentIdFromToken Tests
+
+ [Fact]
+ public void GetAgentIdFromToken_ReturnsEmptyString_WhenTokenIsEmpty()
+ {
+ // Act
+ var result = Utility.GetAgentIdFromToken("");
+
+ // Assert
+ Assert.Equal(string.Empty, result);
+ }
+
+ [Fact]
+ public void GetAgentIdFromToken_ReturnsEmptyString_WhenTokenIsWhitespace()
+ {
+ // Act
+ var result = Utility.GetAgentIdFromToken(" ");
+
+ // Assert
+ Assert.Equal(string.Empty, result);
+ }
+
+ [Fact]
+ public void GetAgentIdFromToken_ReturnsEmptyString_WhenTokenIsNull()
+ {
+ // Act
+ var result = Utility.GetAgentIdFromToken(null!);
+
+ // Assert
+ Assert.Equal(string.Empty, result);
+ }
+
+ [Fact]
+ public void GetAgentIdFromToken_ReturnsBlueprintId_WhenXmsParAppAzpPresent()
+ {
+ // Arrange
+ var token = CreateTestJwtToken(new Claim("xms_par_app_azp", "blueprint-id-123"),
+ new Claim("appid", "app-id-456"),
+ new Claim("azp", "azp-id-789"));
+
+ // Act
+ var result = Utility.GetAgentIdFromToken(token);
+
+ // Assert
+ Assert.Equal("blueprint-id-123", result);
+ }
+
+ [Fact]
+ public void GetAgentIdFromToken_ReturnsAppId_WhenXmsParAppAzpNotPresent()
+ {
+ // Arrange
+ var token = CreateTestJwtToken(new Claim("appid", "app-id-456"),
+ new Claim("azp", "azp-id-789"));
+
+ // Act
+ var result = Utility.GetAgentIdFromToken(token);
+
+ // Assert
+ Assert.Equal("app-id-456", result);
+ }
+
+ [Fact]
+ public void GetAgentIdFromToken_ReturnsAzp_WhenOnlyAzpPresent()
+ {
+ // Arrange
+ var token = CreateTestJwtToken(new Claim("azp", "azp-id-789"));
+
+ // Act
+ var result = Utility.GetAgentIdFromToken(token);
+
+ // Assert
+ Assert.Equal("azp-id-789", result);
+ }
+
+ [Fact]
+ public void GetAgentIdFromToken_ReturnsEmptyString_WhenNoRelevantClaimsPresent()
+ {
+ // Arrange
+ var token = CreateTestJwtToken(new Claim("sub", "some-subject"),
+ new Claim("iss", "some-issuer"));
+
+ // Act
+ var result = Utility.GetAgentIdFromToken(token);
+
+ // Assert
+ Assert.Equal(string.Empty, result);
+ }
+
+ [Fact]
+ public void GetAgentIdFromToken_ReturnsEmptyString_WhenTokenIsMalformed()
+ {
+ // Act
+ var result = Utility.GetAgentIdFromToken("not-a-valid-jwt-token");
+
+ // Assert
+ Assert.Equal(string.Empty, result);
+ }
+
+ [Fact]
+ public void GetAgentIdFromToken_PrefersXmsParAppAzp_OverAppId()
+ {
+ // Arrange - both present, xms_par_app_azp should win
+ var token = CreateTestJwtToken(new Claim("appid", "app-id-first"),
+ new Claim("xms_par_app_azp", "blueprint-id-second"));
+
+ // Act
+ var result = Utility.GetAgentIdFromToken(token);
+
+ // Assert
+ Assert.Equal("blueprint-id-second", result);
+ }
+
+ [Fact]
+ public void GetAgentIdFromToken_FallsBackToAppId_WhenXmsParAppAzpIsEmpty()
+ {
+ // Arrange
+ var token = CreateTestJwtToken(new Claim("xms_par_app_azp", ""),
+ new Claim("appid", "app-id-456"));
+
+ // Act
+ var result = Utility.GetAgentIdFromToken(token);
+
+ // Assert
+ Assert.Equal("app-id-456", result);
+ }
+
+ [Fact]
+ public void GetAgentIdFromToken_FallsBackToAzp_WhenBothXmsParAppAzpAndAppIdAreEmpty()
+ {
+ // Arrange
+ var token = CreateTestJwtToken(new Claim("xms_par_app_azp", ""),
+ new Claim("appid", ""),
+ new Claim("azp", "azp-id-789"));
+
+ // Act
+ var result = Utility.GetAgentIdFromToken(token);
+
+ // Assert
+ Assert.Equal("azp-id-789", result);
+ }
+
+ #endregion
+
+ #region GetApplicationName Tests
+
+ [Fact]
+ public void GetApplicationName_ReturnsAssemblyName()
+ {
+ // Act
+ var result = Utility.GetApplicationName();
+
+ // Assert
+ // In a test context, the entry assembly should exist and have a name
+ // The exact name depends on the test runner, so we just verify it's not null
+ Assert.NotNull(result);
+ Assert.NotEmpty(result);
+ }
+
+ #endregion
+
+ #region Helper Methods
+
+ ///
+ /// Creates a test JWT token with the specified claims.
+ /// Note: This creates an unsigned token suitable for testing claim extraction only.
+ ///
+ private static string CreateTestJwtToken(params Claim[] claims)
+ {
+ var header = Convert.ToBase64String(Encoding.UTF8.GetBytes("{\"alg\":\"none\",\"typ\":\"JWT\"}"))
+ .TrimEnd('=').Replace('+', '-').Replace('/', '_');
+
+ var payloadDict = claims
+ .GroupBy(c => c.Type)
+ .ToDictionary(g => g.Key, g => g.First().Value);
+
+ var payloadJson = System.Text.Json.JsonSerializer.Serialize(payloadDict);
+ var payload = Convert.ToBase64String(Encoding.UTF8.GetBytes(payloadJson))
+ .TrimEnd('=').Replace('+', '-').Replace('/', '_');
+
+ return $"{header}.{payload}.";
+ }
+
+ #endregion
}
}
diff --git a/src/Tooling/Core/Handlers/HttpContextHeadersHandler.cs b/src/Tooling/Core/Handlers/HttpContextHeadersHandler.cs
index 38654f0d..749b0d5d 100644
--- a/src/Tooling/Core/Handlers/HttpContextHeadersHandler.cs
+++ b/src/Tooling/Core/Handlers/HttpContextHeadersHandler.cs
@@ -6,6 +6,7 @@ namespace Microsoft.Agents.A365.Tooling.Handlers
using Microsoft.Extensions.Logging;
using Microsoft.Agents.A365.Runtime;
using Microsoft.Agents.A365.Tooling.Models;
+ using Microsoft.Agents.A365.Tooling.Utils;
using System;
using System.Globalization;
using System.Net.Http;
@@ -13,6 +14,7 @@ namespace Microsoft.Agents.A365.Tooling.Handlers
using System.Text.RegularExpressions;
using System.Threading;
using System.Threading.Tasks;
+ using RuntimeUtility = Microsoft.Agents.A365.Runtime.Utils.Utility;
internal class HttpContextHeadersHandler : DelegatingHandler
{
@@ -32,12 +34,14 @@ internal class HttpContextHeadersHandler : DelegatingHandler
private readonly ITurnContext turnContext;
private readonly ILogger logger;
private readonly ToolOptions toolOptions;
+ private readonly string? authToken;
- public HttpContextHeadersHandler(ITurnContext turnContext, ILogger logger, ToolOptions toolOptions)
+ public HttpContextHeadersHandler(ITurnContext turnContext, ILogger logger, ToolOptions toolOptions, string? authToken = null)
{
this.turnContext = turnContext;
this.logger = logger;
this.toolOptions = toolOptions;
+ this.authToken = authToken;
}
protected override Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
@@ -86,9 +90,72 @@ protected override Task SendAsync(HttpRequestMessage reques
request.Headers.Add(UserAgentHeader, UserAgentHelper.BuildUserAgent(this.toolOptions.UserAgentConfiguration));
}
+ // Add x-ms-agentid header if auth token is available
+ if (!string.IsNullOrEmpty(authToken))
+ {
+ var agentId = ResolveAgentIdForHeader();
+ if (!string.IsNullOrEmpty(agentId))
+ {
+ request.Headers.Add(Constants.Headers.AgentIdHeader, agentId);
+ }
+ }
+
return base.SendAsync(request, cancellationToken);
}
+ ///
+ /// Resolves the best available agent identifier for the x-ms-agentid header.
+ /// Priority: TurnContext.agenticAppBlueprintId > token claims (xms_par_app_azp > appid > azp) > application name
+ ///
+ /// Agent ID string or null if not available.
+ private string? ResolveAgentIdForHeader()
+ {
+ // Priority 1: Agent Blueprint ID from TurnContext
+ // The 'From' property may include agenticAppBlueprintId when the request originates from an agentic app
+ var blueprintId = GetAgenticAppBlueprintIdFromContext();
+ if (!string.IsNullOrEmpty(blueprintId))
+ {
+ return blueprintId;
+ }
+
+ // Priority 2 & 3: Agent ID from token (xms_par_app_azp > appid > azp)
+ // Single decode, checks claims in priority order
+ if (!string.IsNullOrEmpty(authToken))
+ {
+ var agentId = RuntimeUtility.GetAgentIdFromToken(authToken);
+ if (!string.IsNullOrEmpty(agentId))
+ {
+ return agentId;
+ }
+ }
+
+ // Priority 4: Application name from assembly
+ return RuntimeUtility.GetApplicationName();
+ }
+
+ ///
+ /// Gets the agentic app blueprint ID from the turn context if available.
+ ///
+ /// The blueprint ID or null if not available.
+ private string? GetAgenticAppBlueprintIdFromContext()
+ {
+ if (turnContext?.Activity?.From?.Properties == null)
+ {
+ return null;
+ }
+
+ if (turnContext.Activity.From.Properties.TryGetValue("agenticAppBlueprintId", out var blueprintIdElement))
+ {
+ var blueprintId = blueprintIdElement.ToString();
+ if (!string.IsNullOrEmpty(blueprintId))
+ {
+ return blueprintId;
+ }
+ }
+
+ return null;
+ }
+
public static string SanitizeTextForHeader(string input, ILogger logger)
{
try
diff --git a/src/Tooling/Core/Microsoft.Agents.A365.Tooling.csproj b/src/Tooling/Core/Microsoft.Agents.A365.Tooling.csproj
index 7e3d4104..267e06c2 100644
--- a/src/Tooling/Core/Microsoft.Agents.A365.Tooling.csproj
+++ b/src/Tooling/Core/Microsoft.Agents.A365.Tooling.csproj
@@ -12,6 +12,9 @@
Microsoft Agent 365 Tooling SDK
Microsoft;Agent365;A365;Tooling;MCP;AI;Agents
+
+
+
diff --git a/src/Tooling/Core/Services/McpToolServerConfigurationService.cs b/src/Tooling/Core/Services/McpToolServerConfigurationService.cs
index 93d6bb98..db26decb 100644
--- a/src/Tooling/Core/Services/McpToolServerConfigurationService.cs
+++ b/src/Tooling/Core/Services/McpToolServerConfigurationService.cs
@@ -482,7 +482,7 @@ private async Task CreateMcpClientWithAuthHandlers(ITurnContext turn
this._logger.LogInformation($"Configured authentication handler for MCP endpoint {endpoint}");
- var httpContextHeaderHandler = new HttpContextHeadersHandler(turnContext, this._logger, toolOptions)
+ var httpContextHeaderHandler = new HttpContextHeadersHandler(turnContext, this._logger, toolOptions, authToken)
{
InnerHandler = authHandler,
};
diff --git a/src/Tooling/Core/Utils/Constants.cs b/src/Tooling/Core/Utils/Constants.cs
index 21fc2c2e..a3bff780 100644
--- a/src/Tooling/Core/Utils/Constants.cs
+++ b/src/Tooling/Core/Utils/Constants.cs
@@ -1,5 +1,5 @@
-// Copyright (c) Microsoft Corporation.
-// Licensed under the MIT License.
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
namespace Microsoft.Agents.A365.Tooling.Utils
{
@@ -17,6 +17,11 @@ public class Headers
/// The prefix used for Bearer authentication tokens in HTTP headers.
///
public const string BearerPrefix = "Bearer";
+
+ ///
+ /// Header name for sending the agent identifier to MCP platform for logging/analytics.
+ ///
+ public const string AgentIdHeader = "x-ms-agentid";
}
}
}
diff --git a/src/Tooling/Core/docs/prd-x-ms-agentid-header.md b/src/Tooling/Core/docs/prd-x-ms-agentid-header.md
new file mode 100644
index 00000000..2f31d621
--- /dev/null
+++ b/src/Tooling/Core/docs/prd-x-ms-agentid-header.md
@@ -0,0 +1,271 @@
+# PRD: x-ms-agentId Header for MCP Platform Calls
+
+## Overview
+
+Add an `x-ms-agentId` header to all outbound HTTP requests from the tooling package to the MCP platform. This header identifies the calling agent using the best available identifier.
+
+## Problem Statement
+
+The MCP platform needs to identify which agent is making tooling requests for:
+- Logging and diagnostics
+- Usage analytics
+
+Currently, no consistent agent identifier is sent with MCP platform requests.
+
+## Requirements
+
+### Functional Requirements
+
+1. All HTTP requests to the MCP platform SHALL include the `x-ms-agentId` header
+2. The header value SHALL be determined using the following priority:
+ 1. **Agent Blueprint ID from TurnContext** (highest priority) - from `TurnContext.Activity.From.AgenticAppBlueprintId`
+ 2. **Agent Blueprint ID from token** (`xms_par_app_azp` claim)
+ 3. **Entra Application ID from token** (`appid` or `azp` claim)
+ 4. **Application name** (lowest priority fallback) - from assembly name
+3. If no identifier is available, the header SHOULD be omitted (not sent with empty value)
+
+### Non-Functional Requirements
+
+1. No additional network calls to retrieve identifiers
+2. Minimal performance impact on existing flows
+3. Backward compatible - existing integrations continue to work
+
+## Technical Design
+
+### Affected Components
+
+| Package | File | Change |
+|---------|------|--------|
+| `Microsoft.Agents.A365.Runtime` | `Utility.cs` | Add `GetAgentIdFromToken()` method (checks `xms_par_app_azp` → `appid` → `azp`) |
+| `Microsoft.Agents.A365.Runtime` | `Utility.cs` | Add `GetApplicationName()` method (returns assembly name) |
+| `Microsoft.Agents.A365.Tooling` | `Constants.cs` | Add `AgentIdHeader` constant |
+| `Microsoft.Agents.A365.Tooling` | `HttpContextHeadersHandler.cs` | Add `x-ms-agentid` header with priority fallback |
+
+### Identifier Retrieval Strategy
+
+#### 1. Agent Blueprint ID from TurnContext (Highest Priority)
+
+**Source**: `TurnContext.Activity.From.Properties["agenticAppBlueprintId"]`
+
+**Availability**: Only available in agentic request scenarios where a `TurnContext` is present and the request originates from another agent.
+
+**Format**: GUID (e.g., `12345678-1234-1234-1234-123456789abc`)
+
+---
+
+#### 2 & 3. Agent ID from Token (Second/Third Priority)
+
+**Sources** (checked in order):
+1. `xms_par_app_azp` claim - Agent Blueprint ID (parent application's Azure app ID)
+2. `appid` or `azp` claim - Entra Application ID
+
+**Availability**: Available when an `authToken` is provided to the tooling methods.
+
+**Retrieval**: New utility function that decodes token once and checks claims in priority order:
+
+```csharp
+// Microsoft.Agents.A365.Runtime.Utils.Utility
+public static string GetAgentIdFromToken(string token)
+{
+ if (string.IsNullOrWhiteSpace(token))
+ {
+ return string.Empty;
+ }
+
+ try
+ {
+ var handler = new JwtSecurityTokenHandler();
+ var jwtToken = handler.ReadJwtToken(token);
+
+ // Priority: xms_par_app_azp (blueprint ID) > appid > azp
+ var blueprintClaim = jwtToken.Claims.FirstOrDefault(c => c.Type == "xms_par_app_azp");
+ if (!string.IsNullOrEmpty(blueprintClaim?.Value))
+ {
+ return blueprintClaim.Value;
+ }
+
+ var appIdClaim = jwtToken.Claims.FirstOrDefault(c => c.Type == "appid");
+ if (!string.IsNullOrEmpty(appIdClaim?.Value))
+ {
+ return appIdClaim.Value;
+ }
+
+ var azpClaim = jwtToken.Claims.FirstOrDefault(c => c.Type == "azp");
+ return azpClaim?.Value ?? string.Empty;
+ }
+ catch
+ {
+ return string.Empty;
+ }
+}
+```
+
+**Format**: GUID (e.g., `12345678-1234-1234-1234-123456789abc`)
+
+---
+
+#### 4. Application Name (Lowest Priority Fallback)
+
+**Source**: Entry assembly name
+
+**Strategy**:
+1. Get the entry assembly name via `Assembly.GetEntryAssembly()?.GetName()?.Name`
+2. If not available, omit the header
+
+**Implementation**:
+```csharp
+// Microsoft.Agents.A365.Runtime.Utils.Utility
+public static string? GetApplicationName()
+{
+ return Assembly.GetEntryAssembly()?.GetName()?.Name;
+}
+```
+
+---
+
+### Implementation
+
+#### Updated HttpContextHeadersHandler
+
+The `x-ms-agentid` header will be added in the `HttpContextHeadersHandler` which already handles other context headers. The header is only added when the auth token is available (via the `BearerTokenHandler` in the chain).
+
+```csharp
+// Microsoft.Agents.A365.Tooling.Handlers.HttpContextHeadersHandler
+internal class HttpContextHeadersHandler : DelegatingHandler
+{
+ private const string AgentIdHeader = "x-ms-agentid";
+ // ... existing headers ...
+
+ private readonly string? authToken;
+
+ public HttpContextHeadersHandler(ITurnContext turnContext, ILogger logger, ToolOptions toolOptions, string? authToken = null)
+ {
+ this.turnContext = turnContext;
+ this.logger = logger;
+ this.toolOptions = toolOptions;
+ this.authToken = authToken;
+ }
+
+ protected override Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
+ {
+ // ... existing header logic ...
+
+ // Add x-ms-agentid header if auth token is available
+ if (!string.IsNullOrEmpty(authToken))
+ {
+ var agentId = ResolveAgentIdForHeader();
+ if (!string.IsNullOrEmpty(agentId))
+ {
+ request.Headers.Add(AgentIdHeader, agentId);
+ }
+ }
+
+ return base.SendAsync(request, cancellationToken);
+ }
+
+ private string? ResolveAgentIdForHeader()
+ {
+ // Priority 1: Agent Blueprint ID from TurnContext
+ var blueprintId = GetAgenticAppBlueprintIdFromContext();
+ if (!string.IsNullOrEmpty(blueprintId))
+ {
+ return blueprintId;
+ }
+
+ // Priority 2 & 3: Agent ID from token (xms_par_app_azp > appid > azp)
+ if (!string.IsNullOrEmpty(authToken))
+ {
+ var agentId = RuntimeUtility.GetAgentIdFromToken(authToken);
+ if (!string.IsNullOrEmpty(agentId))
+ {
+ return agentId;
+ }
+ }
+
+ // Priority 4: Application name from assembly
+ return RuntimeUtility.GetApplicationName();
+ }
+}
+```
+
+### Call Sites Summary
+
+| Call Site | authToken | turnContext | Gets `x-ms-agentid`? |
+|-----------|-----------|-------------|----------------------|
+| `GetMCPServerFromToolingGatewayAsync()` | ✅ | ❌ (not passed) | ✅ Yes (via new overload) |
+| `GetMcpClientToolsAsync()` | ✅ | ✅ | ✅ Yes |
+| `SendChatHistoryAsync()` | ❌ | ✅ | ❌ No (authToken required) |
+
+**Note**: The `x-ms-agentid` header is only added when `authToken` is present. `SendChatHistoryAsync()` does not use authentication, so it won't include this header.
+
+---
+
+## Open Questions
+
+### Q1: Application Name Strategy ✅ RESOLVED
+
+**Decision**: Use `Assembly.GetEntryAssembly()?.GetName()?.Name` as the .NET equivalent of the Node.js npm_package_name.
+
+### Q2: Header Name Casing ✅ RESOLVED
+
+**Decision**: Use `x-ms-agentid` (all lowercase, case insensitive).
+
+HTTP headers are case-insensitive per RFC 7230, so the server will accept any casing. Using lowercase is the conventional choice.
+
+### Q3: Missing Identifier Behavior ✅ RESOLVED
+
+**Decision**: Omit the header entirely if no identifier is available. Do not send empty or "unknown" values.
+
+---
+
+## Testing Strategy
+
+### Unit Tests
+
+1. Test `GetAgentIdFromToken()` checks claims in correct priority order (`xms_par_app_azp` > `appid` > `azp`)
+2. Test `GetAgentIdFromToken()` returns empty string for empty/invalid tokens
+3. Test `GetApplicationName()` returns assembly name
+4. Test `HttpContextHeadersHandler` includes `x-ms-agentid` when identifier available
+5. Test `HttpContextHeadersHandler` omits header when no identifier available
+6. Test priority order: TurnContext > token claims > application name
+
+### Integration Tests
+
+1. Verify header is sent in `GetMcpClientToolsAsync()` requests
+2. Verify header is NOT sent in `SendChatHistoryAsync()` requests (no authToken)
+
+---
+
+## Breaking Changes
+
+**None** - This implementation is fully backward compatible.
+
+### Migration Guide
+
+**For existing consumers:**
+- No changes required - existing code continues to work
+- The `x-ms-agentid` header will automatically be included in requests where authentication is used
+
+---
+
+## Rollout Plan
+
+1. **Phase 1**: Add utility methods to Runtime package
+2. **Phase 2**: Update `HttpContextHeadersHandler` to add `x-ms-agentid` header
+3. **Phase 3**: Update documentation
+
+---
+
+## Dependencies
+
+- Runtime package for `GetAppIdFromToken()` utility (already exists)
+- `System.IdentityModel.Tokens.Jwt` for JWT decoding (already referenced)
+- No new external dependencies required
+
+---
+
+## Success Metrics
+
+1. 100% of MCP platform requests include `x-ms-agentId` header (when identifier available)
+2. No increase in request latency
+3. No breaking changes for existing consumers