Skip to content

Commit 2b66ca0

Browse files
.NET: Fix Error 404 Agent Hosted MCP (#3678)
* Initial plan * Fix issue #3195: Handle empty Version and ID in Azure AI agent responses This fix addresses the issue where hosted MCP agents (like AgentWithHostedMCP) fail with "ID cannot be null or empty (Parameter 'id')" error when deployed to Azure AI Foundry. Changes: - Add CreateAgentReference helper method in AzureAIProjectChatClient that defaults empty version to "latest" - Update CreateChatClientAgentOptions to generate a fallback ID from name and version when AgentVersion.Id is null or empty - Add GetAgentVersionResponseJsonWithEmptyVersion and GetAgentResponseJsonWithEmptyVersion test data methods - Add unit tests for empty version handling scenarios Co-authored-by: rogerbarreto <19890735+rogerbarreto@users.noreply.github.com> * Address code review feedback: improve documentation and test comments Co-authored-by: rogerbarreto <19890735+rogerbarreto@users.noreply.github.com> * Address PR review: Use IsNullOrWhiteSpace and add whitespace unit tests --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
1 parent aa88195 commit 2b66ca0

4 files changed

Lines changed: 308 additions & 13 deletions

File tree

dotnet/src/Microsoft.Agents.AI.AzureAI/AzureAIProjectChatClient.cs

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,13 +64,27 @@ internal AzureAIProjectChatClient(AIProjectClient aiProjectClient, AgentRecord a
6464
internal AzureAIProjectChatClient(AIProjectClient aiProjectClient, AgentVersion agentVersion, ChatOptions? chatOptions)
6565
: this(
6666
aiProjectClient,
67-
new AgentReference(Throw.IfNull(agentVersion).Name, agentVersion.Version),
67+
CreateAgentReference(Throw.IfNull(agentVersion)),
6868
(agentVersion.Definition as PromptAgentDefinition)?.Model,
6969
chatOptions)
7070
{
7171
this._agentVersion = agentVersion;
7272
}
7373

74+
/// <summary>
75+
/// Creates an <see cref="AgentReference"/> from an <see cref="AgentVersion"/>.
76+
/// Uses the agent version's version if available, otherwise defaults to "latest".
77+
/// </summary>
78+
/// <param name="agentVersion">The agent version to create a reference from.</param>
79+
/// <returns>An <see cref="AgentReference"/> for the specified agent version.</returns>
80+
private static AgentReference CreateAgentReference(AgentVersion agentVersion)
81+
{
82+
// If the version is null, empty, or whitespace, use "latest" as the default.
83+
// This handles cases where hosted agents (like MCP agents) may not have a version assigned.
84+
var version = string.IsNullOrWhiteSpace(agentVersion.Version) ? "latest" : agentVersion.Version;
85+
return new AgentReference(agentVersion.Name, version);
86+
}
87+
7488
/// <inheritdoc/>
7589
public override object? GetService(Type serviceType, object? serviceKey = null)
7690
{

dotnet/src/Microsoft.Agents.AI.AzureAI/AzureAIProjectChatClientExtensions.cs

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -543,9 +543,16 @@ private static ChatClientAgentOptions CreateChatClientAgentOptions(AgentVersion
543543
}
544544
}
545545

546+
// Use the agent version's ID if available, otherwise generate one from name and version.
547+
// This handles cases where hosted agents (like MCP agents) may not have an ID assigned.
548+
var version = string.IsNullOrWhiteSpace(agentVersion.Version) ? "latest" : agentVersion.Version;
549+
var agentId = string.IsNullOrWhiteSpace(agentVersion.Id)
550+
? $"{agentVersion.Name}:{version}"
551+
: agentVersion.Id;
552+
546553
var agentOptions = new ChatClientAgentOptions()
547554
{
548-
Id = agentVersion.Id,
555+
Id = agentId,
549556
Name = agentVersion.Name,
550557
Description = agentVersion.Description,
551558
};

dotnet/tests/Microsoft.Agents.AI.AzureAI.UnitTests/AzureAIProjectChatClientExtensionsTests.cs

Lines changed: 221 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -2384,6 +2384,134 @@ public async Task GetAIAgentAsync_WithUseProvidedChatClientAsIs_PreservesSetting
23842384

23852385
#endregion
23862386

2387+
#region Empty Version and ID Handling Tests
2388+
2389+
/// <summary>
2390+
/// Verify that GetAIAgentAsync handles an agent with empty version by using "latest" as fallback.
2391+
/// </summary>
2392+
[Fact]
2393+
public async Task GetAIAgentAsync_WithEmptyVersion_CreatesAgentSuccessfullyAsync()
2394+
{
2395+
// Arrange
2396+
AIProjectClient client = this.CreateTestAgentClientWithEmptyVersion();
2397+
var options = new ChatClientAgentOptions
2398+
{
2399+
Name = "test-agent",
2400+
ChatOptions = new ChatOptions { Instructions = "Test" }
2401+
};
2402+
2403+
// Act
2404+
ChatClientAgent agent = await client.GetAIAgentAsync(options);
2405+
2406+
// Assert
2407+
Assert.NotNull(agent);
2408+
Assert.IsType<ChatClientAgent>(agent);
2409+
// Verify the agent ID is generated from server-returned name ("agent_abc123") and "latest"
2410+
Assert.Equal("agent_abc123:latest", agent.Id);
2411+
}
2412+
2413+
/// <summary>
2414+
/// Verify that AsAIAgent with AgentRecord handles empty version by using "latest" as fallback.
2415+
/// </summary>
2416+
[Fact]
2417+
public void AsAIAgent_WithAgentRecordEmptyVersion_CreatesAgentWithGeneratedId()
2418+
{
2419+
// Arrange
2420+
AIProjectClient client = this.CreateTestAgentClientWithEmptyVersion();
2421+
AgentRecord agentRecord = this.CreateTestAgentRecordWithEmptyVersion();
2422+
2423+
// Act
2424+
var agent = client.AsAIAgent(agentRecord);
2425+
2426+
// Assert
2427+
Assert.NotNull(agent);
2428+
// Verify the agent ID is generated from agent record name ("agent_abc123") and "latest"
2429+
Assert.Equal("agent_abc123:latest", agent.Id);
2430+
}
2431+
2432+
/// <summary>
2433+
/// Verify that AsAIAgent with AgentVersion handles empty version by using "latest" as fallback.
2434+
/// </summary>
2435+
[Fact]
2436+
public void AsAIAgent_WithAgentVersionEmptyVersion_CreatesAgentWithGeneratedId()
2437+
{
2438+
// Arrange
2439+
AIProjectClient client = this.CreateTestAgentClientWithEmptyVersion();
2440+
AgentVersion agentVersion = this.CreateTestAgentVersionWithEmptyVersion();
2441+
2442+
// Act
2443+
var agent = client.AsAIAgent(agentVersion);
2444+
2445+
// Assert
2446+
Assert.NotNull(agent);
2447+
// Verify the agent ID is generated from agent version name ("agent_abc123") and "latest"
2448+
Assert.Equal("agent_abc123:latest", agent.Id);
2449+
}
2450+
2451+
/// <summary>
2452+
/// Verify that GetAIAgentAsync handles an agent with whitespace-only version by using "latest" as fallback.
2453+
/// </summary>
2454+
[Fact]
2455+
public async Task GetAIAgentAsync_WithWhitespaceVersion_CreatesAgentSuccessfullyAsync()
2456+
{
2457+
// Arrange
2458+
AIProjectClient client = this.CreateTestAgentClientWithWhitespaceVersion();
2459+
var options = new ChatClientAgentOptions
2460+
{
2461+
Name = "test-agent",
2462+
ChatOptions = new ChatOptions { Instructions = "Test" }
2463+
};
2464+
2465+
// Act
2466+
ChatClientAgent agent = await client.GetAIAgentAsync(options);
2467+
2468+
// Assert
2469+
Assert.NotNull(agent);
2470+
Assert.IsType<ChatClientAgent>(agent);
2471+
// Verify the agent ID is generated from server-returned name ("agent_abc123") and "latest"
2472+
Assert.Equal("agent_abc123:latest", agent.Id);
2473+
}
2474+
2475+
/// <summary>
2476+
/// Verify that AsAIAgent with AgentRecord handles whitespace-only version by using "latest" as fallback.
2477+
/// </summary>
2478+
[Fact]
2479+
public void AsAIAgent_WithAgentRecordWhitespaceVersion_CreatesAgentWithGeneratedId()
2480+
{
2481+
// Arrange
2482+
AIProjectClient client = this.CreateTestAgentClientWithWhitespaceVersion();
2483+
AgentRecord agentRecord = this.CreateTestAgentRecordWithWhitespaceVersion();
2484+
2485+
// Act
2486+
var agent = client.AsAIAgent(agentRecord);
2487+
2488+
// Assert
2489+
Assert.NotNull(agent);
2490+
// Verify the agent ID is generated from agent record name ("agent_abc123") and "latest"
2491+
Assert.Equal("agent_abc123:latest", agent.Id);
2492+
}
2493+
2494+
/// <summary>
2495+
/// Verify that AsAIAgent with AgentVersion handles whitespace-only version by using "latest" as fallback.
2496+
/// </summary>
2497+
[Fact]
2498+
public void AsAIAgent_WithAgentVersionWhitespaceVersion_CreatesAgentWithGeneratedId()
2499+
{
2500+
// Arrange
2501+
AIProjectClient client = this.CreateTestAgentClientWithWhitespaceVersion();
2502+
AgentVersion agentVersion = this.CreateTestAgentVersionWithWhitespaceVersion();
2503+
2504+
// Act
2505+
var agent = client.AsAIAgent(agentVersion);
2506+
2507+
// Assert
2508+
Assert.NotNull(agent);
2509+
// Verify the agent ID is generated from agent version name ("agent_abc123") and "latest"
2510+
Assert.Equal("agent_abc123:latest", agent.Id);
2511+
}
2512+
2513+
#endregion
2514+
23872515
#region ApplyToolsToAgentDefinition Tests
23882516

23892517
/// <summary>
@@ -2678,6 +2806,54 @@ private AgentRecord CreateTestAgentRecord(AgentDefinition? agentDefinition = nul
26782806
return ModelReaderWriter.Read<AgentRecord>(BinaryData.FromString(TestDataUtil.GetAgentResponseJson(agentDefinition: agentDefinition)))!;
26792807
}
26802808

2809+
/// <summary>
2810+
/// Creates a test AIProjectClient with empty version fields for testing hosted MCP agents.
2811+
/// </summary>
2812+
private FakeAgentClient CreateTestAgentClientWithEmptyVersion(string? agentName = null, string? instructions = null, string? description = null, AgentDefinition? agentDefinitionResponse = null)
2813+
{
2814+
return new FakeAgentClient(agentName, instructions, description, agentDefinitionResponse, useEmptyVersion: true);
2815+
}
2816+
2817+
/// <summary>
2818+
/// Creates a test AgentRecord with empty version for testing hosted MCP agents.
2819+
/// </summary>
2820+
private AgentRecord CreateTestAgentRecordWithEmptyVersion(AgentDefinition? agentDefinition = null)
2821+
{
2822+
return ModelReaderWriter.Read<AgentRecord>(BinaryData.FromString(TestDataUtil.GetAgentResponseJsonWithEmptyVersion(agentDefinition: agentDefinition)))!;
2823+
}
2824+
2825+
/// <summary>
2826+
/// Creates a test AgentVersion with empty version for testing hosted MCP agents.
2827+
/// </summary>
2828+
private AgentVersion CreateTestAgentVersionWithEmptyVersion()
2829+
{
2830+
return ModelReaderWriter.Read<AgentVersion>(BinaryData.FromString(TestDataUtil.GetAgentVersionResponseJsonWithEmptyVersion()))!;
2831+
}
2832+
2833+
/// <summary>
2834+
/// Creates a test AIProjectClient with whitespace-only version fields for testing hosted MCP agents.
2835+
/// </summary>
2836+
private FakeAgentClient CreateTestAgentClientWithWhitespaceVersion(string? agentName = null, string? instructions = null, string? description = null, AgentDefinition? agentDefinitionResponse = null)
2837+
{
2838+
return new FakeAgentClient(agentName, instructions, description, agentDefinitionResponse, versionMode: VersionMode.Whitespace);
2839+
}
2840+
2841+
/// <summary>
2842+
/// Creates a test AgentRecord with whitespace-only version for testing hosted MCP agents.
2843+
/// </summary>
2844+
private AgentRecord CreateTestAgentRecordWithWhitespaceVersion(AgentDefinition? agentDefinition = null)
2845+
{
2846+
return ModelReaderWriter.Read<AgentRecord>(BinaryData.FromString(TestDataUtil.GetAgentResponseJsonWithWhitespaceVersion(agentDefinition: agentDefinition)))!;
2847+
}
2848+
2849+
/// <summary>
2850+
/// Creates a test AgentVersion with whitespace-only version for testing hosted MCP agents.
2851+
/// </summary>
2852+
private AgentVersion CreateTestAgentVersionWithWhitespaceVersion()
2853+
{
2854+
return ModelReaderWriter.Read<AgentVersion>(BinaryData.FromString(TestDataUtil.GetAgentVersionResponseJsonWithWhitespaceVersion()))!;
2855+
}
2856+
26812857
private const string OpenAPISpec = """
26822858
{
26832859
"openapi": "3.0.3",
@@ -2716,14 +2892,26 @@ private AgentVersion CreateTestAgentVersion()
27162892
return ModelReaderWriter.Read<AgentVersion>(BinaryData.FromString(TestDataUtil.GetAgentVersionResponseJson()))!;
27172893
}
27182894

2895+
/// <summary>
2896+
/// Specifies the version mode for test data generation.
2897+
/// </summary>
2898+
private enum VersionMode
2899+
{
2900+
Normal,
2901+
Empty,
2902+
Whitespace
2903+
}
2904+
27192905
/// <summary>
27202906
/// Fake AIProjectClient for testing.
27212907
/// </summary>
27222908
private sealed class FakeAgentClient : AIProjectClient
27232909
{
2724-
public FakeAgentClient(string? agentName = null, string? instructions = null, string? description = null, AgentDefinition? agentDefinitionResponse = null)
2910+
public FakeAgentClient(string? agentName = null, string? instructions = null, string? description = null, AgentDefinition? agentDefinitionResponse = null, bool useEmptyVersion = false, VersionMode versionMode = VersionMode.Normal)
27252911
{
2726-
this.Agents = new FakeAIProjectAgentsOperations(agentName, instructions, description, agentDefinitionResponse);
2912+
// Handle backward compatibility with bool parameter
2913+
var effectiveVersionMode = useEmptyVersion ? VersionMode.Empty : versionMode;
2914+
this.Agents = new FakeAIProjectAgentsOperations(agentName, instructions, description, agentDefinitionResponse, effectiveVersionMode);
27272915
}
27282916

27292917
public override ClientConnection GetConnection(string connectionId)
@@ -2739,60 +2927,82 @@ private sealed class FakeAIProjectAgentsOperations : AIProjectAgentsOperations
27392927
private readonly string? _instructions;
27402928
private readonly string? _description;
27412929
private readonly AgentDefinition? _agentDefinition;
2930+
private readonly VersionMode _versionMode;
27422931

2743-
public FakeAIProjectAgentsOperations(string? agentName = null, string? instructions = null, string? description = null, AgentDefinition? agentDefinitionResponse = null)
2932+
public FakeAIProjectAgentsOperations(string? agentName = null, string? instructions = null, string? description = null, AgentDefinition? agentDefinitionResponse = null, VersionMode versionMode = VersionMode.Normal)
27442933
{
27452934
this._agentName = agentName;
27462935
this._instructions = instructions;
27472936
this._description = description;
27482937
this._agentDefinition = agentDefinitionResponse;
2938+
this._versionMode = versionMode;
2939+
}
2940+
2941+
private string GetAgentResponseJson()
2942+
{
2943+
return this._versionMode switch
2944+
{
2945+
VersionMode.Empty => TestDataUtil.GetAgentResponseJsonWithEmptyVersion(this._agentName, this._agentDefinition, this._instructions, this._description),
2946+
VersionMode.Whitespace => TestDataUtil.GetAgentResponseJsonWithWhitespaceVersion(this._agentName, this._agentDefinition, this._instructions, this._description),
2947+
_ => TestDataUtil.GetAgentResponseJson(this._agentName, this._agentDefinition, this._instructions, this._description)
2948+
};
2949+
}
2950+
2951+
private string GetAgentVersionResponseJson()
2952+
{
2953+
return this._versionMode switch
2954+
{
2955+
VersionMode.Empty => TestDataUtil.GetAgentVersionResponseJsonWithEmptyVersion(this._agentName, this._agentDefinition, this._instructions, this._description),
2956+
VersionMode.Whitespace => TestDataUtil.GetAgentVersionResponseJsonWithWhitespaceVersion(this._agentName, this._agentDefinition, this._instructions, this._description),
2957+
_ => TestDataUtil.GetAgentVersionResponseJson(this._agentName, this._agentDefinition, this._instructions, this._description)
2958+
};
27492959
}
27502960

27512961
public override ClientResult GetAgent(string agentName, RequestOptions options)
27522962
{
2753-
var responseJson = TestDataUtil.GetAgentResponseJson(this._agentName, this._agentDefinition, this._instructions, this._description);
2963+
var responseJson = this.GetAgentResponseJson();
27542964
return ClientResult.FromValue(ModelReaderWriter.Read<AgentRecord>(BinaryData.FromString(responseJson))!, new MockPipelineResponse(200, BinaryData.FromString(responseJson)));
27552965
}
27562966

27572967
public override ClientResult<AgentRecord> GetAgent(string agentName, CancellationToken cancellationToken = default)
27582968
{
2759-
var responseJson = TestDataUtil.GetAgentResponseJson(this._agentName, this._agentDefinition, this._instructions, this._description);
2969+
var responseJson = this.GetAgentResponseJson();
27602970
return ClientResult.FromValue(ModelReaderWriter.Read<AgentRecord>(BinaryData.FromString(responseJson))!, new MockPipelineResponse(200));
27612971
}
27622972

27632973
public override Task<ClientResult> GetAgentAsync(string agentName, RequestOptions options)
27642974
{
2765-
var responseJson = TestDataUtil.GetAgentResponseJson(this._agentName, this._agentDefinition, this._instructions, this._description);
2975+
var responseJson = this.GetAgentResponseJson();
27662976
return Task.FromResult<ClientResult>(ClientResult.FromValue(ModelReaderWriter.Read<AgentRecord>(BinaryData.FromString(responseJson))!, new MockPipelineResponse(200, BinaryData.FromString(responseJson))));
27672977
}
27682978

27692979
public override Task<ClientResult<AgentRecord>> GetAgentAsync(string agentName, CancellationToken cancellationToken = default)
27702980
{
2771-
var responseJson = TestDataUtil.GetAgentResponseJson(this._agentName, this._agentDefinition, this._instructions, this._description);
2981+
var responseJson = this.GetAgentResponseJson();
27722982
return Task.FromResult(ClientResult.FromValue(ModelReaderWriter.Read<AgentRecord>(BinaryData.FromString(responseJson))!, new MockPipelineResponse(200)));
27732983
}
27742984

27752985
public override ClientResult CreateAgentVersion(string agentName, BinaryContent content, RequestOptions? options = null)
27762986
{
2777-
var responseJson = TestDataUtil.GetAgentVersionResponseJson(this._agentName, this._agentDefinition, this._instructions, this._description);
2987+
var responseJson = this.GetAgentVersionResponseJson();
27782988
return ClientResult.FromValue(ModelReaderWriter.Read<AgentVersion>(BinaryData.FromString(responseJson))!, new MockPipelineResponse(200, BinaryData.FromString(responseJson)));
27792989
}
27802990

27812991
public override ClientResult<AgentVersion> CreateAgentVersion(string agentName, AgentVersionCreationOptions? options = null, CancellationToken cancellationToken = default)
27822992
{
2783-
var responseJson = TestDataUtil.GetAgentVersionResponseJson(this._agentName, this._agentDefinition, this._instructions, this._description);
2993+
var responseJson = this.GetAgentVersionResponseJson();
27842994
return ClientResult.FromValue(ModelReaderWriter.Read<AgentVersion>(BinaryData.FromString(responseJson))!, new MockPipelineResponse(200));
27852995
}
27862996

27872997
public override Task<ClientResult> CreateAgentVersionAsync(string agentName, BinaryContent content, RequestOptions? options = null)
27882998
{
2789-
var responseJson = TestDataUtil.GetAgentVersionResponseJson(this._agentName, this._agentDefinition, this._instructions, this._description);
2999+
var responseJson = this.GetAgentVersionResponseJson();
27903000
return Task.FromResult<ClientResult>(ClientResult.FromValue(ModelReaderWriter.Read<AgentVersion>(BinaryData.FromString(responseJson))!, new MockPipelineResponse(200, BinaryData.FromString(responseJson))));
27913001
}
27923002

27933003
public override Task<ClientResult<AgentVersion>> CreateAgentVersionAsync(string agentName, AgentVersionCreationOptions? options = null, CancellationToken cancellationToken = default)
27943004
{
2795-
var responseJson = TestDataUtil.GetAgentVersionResponseJson(this._agentName, this._agentDefinition, this._instructions, this._description);
3005+
var responseJson = this.GetAgentVersionResponseJson();
27963006
return Task.FromResult(ClientResult.FromValue(ModelReaderWriter.Read<AgentVersion>(BinaryData.FromString(responseJson))!, new MockPipelineResponse(200)));
27973007
}
27983008
}

0 commit comments

Comments
 (0)