Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
06b5309
feat: add MCP V1-to-V2 migration support with hardcoded V2 catalog fa…
biswapm Mar 30, 2026
d134dbe
feat: add GetScopesByAudienceAsync and --remove-legacy-scopes for V1/…
biswapm Mar 30, 2026
07f1613
feat: add V1/V2 migration safety guards for add-mcp-servers, permissi…
biswapm Mar 30, 2026
37e6c4c
feat: propagate publisher field through manifest write and list-confi…
biswapm Mar 30, 2026
5d38322
feat: replace hardcoded V2 catalog with live V2 discover endpoint
biswapm Apr 1, 2026
eadac86
Fix 1 — ManifestHelper.cs: Both GetRequiredScopesAsync and GetScopesB…
biswapm Apr 3, 2026
3126b2a
docs: update CHANGELOG for MCP V1/V2 migration and null scope fixes
biswapm Apr 3, 2026
fdeef1b
feat: V2 per-server token writes, default audience fix, and dry-run i…
biswapm Apr 9, 2026
f23eeaa
Renamed Agent365ToolsProdAppId → WorkIQToolsProdAppId which was alrea…
biswapm Apr 9, 2026
f28c282
feat: update SP provisioning script to support MCP V1 and V2 models
biswapm Apr 12, 2026
28fd856
fix: switch ProductionDiscoverEndpointUrl to production endpoint
biswapm Apr 14, 2026
20f2aa4
chore: merge main — resolve conflicts in McpConstants and SP provisio…
biswapm Apr 14, 2026
046f92c
Update scripts/cli/Auth/New-Agent365ToolsServicePrincipalProdPublic.ps1
biswapm Apr 14, 2026
9c1db4d
fixed warning messages
biswapm Apr 14, 2026
7d005cd
Update src/Microsoft.Agents.A365.DevTools.Cli/Helpers/ManifestHelper.cs
biswapm Apr 14, 2026
74a4b60
Update src/Microsoft.Agents.A365.DevTools.Cli/Constants/ConfigConstan…
biswapm Apr 14, 2026
932f97a
Update src/Microsoft.Agents.A365.DevTools.Cli/Helpers/ManifestHelper.cs
biswapm Apr 14, 2026
cec3b19
reverting changes
biswapm Apr 14, 2026
f8f8ae2
fix: pass normalizedContent to WriteCatalog to avoid double normaliza…
Copilot Apr 14, 2026
b6f2c42
fix: address PR review comments on MCP V1-V2 migration
biswapm Apr 16, 2026
7293b15
fix: setup all/admin now use per-audience MCP spec list (V1+V2 support)
biswapm Apr 16, 2026
7dd9d40
fix: route --remove-legacy-scopes confirmation through IConfirmationP…
biswapm Apr 16, 2026
3c06db1
fix: update PermissionsSubcommandTests to pass IConfirmationProvider
biswapm Apr 16, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -175,7 +175,10 @@ private static async Task<bool> CallDiscoverToolServersAsync(IConfigService conf
logger.LogError("Failed to call discoverToolServers endpoint. Status: {Status}", response.StatusCode);
var errorContent = await response.Content.ReadAsStringAsync();
logger.LogError("Error response: {Error}", errorContent);
return false;
// Temporary: fall back to hardcoded V2 catalog until live endpoint is available (Q1).
logger.LogWarning("Falling back to hardcoded V2 catalog for development.");
Services.Internal.McpServerCatalogWriter.WriteHardcodedV2Catalog();
return true;
}

var responseContent = await response.Content.ReadAsStringAsync();
Expand Down Expand Up @@ -456,6 +459,14 @@ private static Command CreateAddMcpServersSubcommand(ILogger logger, IConfigServ
await CallDiscoverToolServersAsync(configService, false, logger, authService, skipLogs: true);
}

// Second guard: if the API call failed and the file still doesn't exist, use hardcoded V2 catalog.
// Temporary — remove once discoverToolServers?api-version=2 is confirmed live (Q1).
if (!File.Exists(catalogPath))
{
logger.LogWarning("Catalog not available from API. Using hardcoded V2 catalog for development.");
Services.Internal.McpServerCatalogWriter.WriteHardcodedV2Catalog();
}

var catalogJson = await File.ReadAllTextAsync(catalogPath);
using var doc = JsonDocument.Parse(catalogJson);
var serversElement = doc.RootElement.GetProperty("mcpServers");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,19 @@ public static class McpConstants
/// </summary>
public const string ListToolServersToolName = "ListToolServers";

/// <summary>
/// Scope value used by all V2 MCP server entries (per-server AppId model)
/// </summary>
public const string V2ScopeValue = "Tools.ListInvoke.All";

/// <summary>
/// Returns true when the scope matches the V1 pattern McpServers.*.All (shared ATG AppId model)
/// </summary>
public static bool IsV1Scope(string? scope) =>
!string.IsNullOrEmpty(scope) &&
scope.StartsWith("McpServers.", StringComparison.OrdinalIgnoreCase) &&
scope.EndsWith(".All", StringComparison.OrdinalIgnoreCase);

// HTTP Headers
public static class MediaTypes
{
Expand Down Expand Up @@ -70,6 +83,8 @@ public static class ManifestProperties
public const string Url = "url";
public const string Scope = "scope";
public const string Audience = "audience";
public const string Id = "id";
public const string Publisher = "publisher";
}

// MCP Server to Entra Scope mappings
Expand Down Expand Up @@ -146,6 +161,59 @@ public static string[] GetAllScopes()
}
}

// Hardcoded V2 server catalog — temporary until discoverToolServers?api-version=2 is live (Q1 open question).
// Wrapped in the { "mcpServers": [...] } envelope that McpServerCatalogWriter and add-mcp-servers expect.
// Remove WrappedJson and WriteHardcodedV2Catalog once the live V2 endpoint is confirmed.
public static class V2Catalog
{
public const string WrappedJson = """
{
"mcpServers": [
{
"mcpServerName": "mcp_ODSPRemoteServer",
"id": "b87204eb-e6cb-491d-a9b5-84cdfd9ad3af",
"url": "https://test.agent365.svc.cloud.dev.microsoft/agents/servers/mcp_ODSPRemoteServer",
"scope": "McpServers.OneDriveSharepoint.All",
"audience": "05879165-0320-489e-b644-f72b33f3edf0",
"publisher": "Microsoft"
},
{
"mcpServerName": "mcp_TeamsServer",
"id": "3fa2b1d9-6e2c-52b9-be4f-95148edff98e",
"url": "https://test.agent365.svc.cloud.dev.microsoft/agents/servers/mcp_TeamsServer",
"scope": "Tools.ListInvoke.All",
"audience": "2cc60bb0-1024-48c8-95f0-1fce211a04d8",
"publisher": "Microsoft"
},
{
"mcpServerName": "mcp_WordServer",
"id": "bc3e6ae5-37ef-4949-9a1d-29828b068b8b",
"url": "https://test.agent365.svc.cloud.dev.microsoft/agents/servers/mcp_WordServer",
"scope": "Tools.ListInvoke.All",
"audience": "ee0064db-2cb5-4174-aa2a-bd3dd879a7d7",
"publisher": "Microsoft"
},
{
"mcpServerName": "mcp_MailTools",
"id": "3fb34f44-7f4e-4e9e-855f-072404166824",
"url": "https://test.agent365.svc.cloud.dev.microsoft/agents/servers/mcp_MailTools",
"scope": "McpServers.Mail.All",
"audience": "05879165-0320-489e-b644-f72b33f3edf0",
"publisher": "Microsoft"
},
{
"mcpServerName": "mcp_CalendarTools",
"id": "87dc802d-1067-427e-97c2-ebf07f8799a4",
"url": "https://test.agent365.svc.cloud.dev.microsoft/agents/servers/mcp_CalendarTools",
"scope": "McpServers.Calendar.All",
"audience": "05879165-0320-489e-b644-f72b33f3edf0",
"publisher": "Microsoft"
}
]
}
""";
}

// PackageMCPServer constants
public static class PackageMCPServer
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,19 @@ public class McpServerConfig
/// </summary>
[JsonPropertyName("audience")]
public string? Audience { get; set; }


/// <summary>
/// The unique server identifier (V2 schema — GUID assigned by the platform)
/// </summary>
[JsonPropertyName("id")]
public string? Id { get; set; }

/// <summary>
/// The publisher of this MCP server (V2 schema — e.g. "Microsoft")
/// </summary>
[JsonPropertyName("publisher")]
public string? Publisher { get; set; }

/// <summary>
/// Optional description of the MCP server's capabilities
/// </summary>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,17 +1,36 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

using Microsoft.Agents.A365.DevTools.Cli.Constants;

namespace Microsoft.Agents.A365.DevTools.Cli.Services.Internal;

public static class McpServerCatalogWriter
{
public static string WriteCatalog(string responseContent)
{
// V2 endpoint returns a raw JSON array [...].
// V1 returns { "mcpServers": [...] }.
// Normalize both to the wrapped format so all callers remain unchanged.
if (responseContent.TrimStart().StartsWith('['))
{
responseContent = $"{{\"mcpServers\":{responseContent}}}";
Comment thread
biswapm marked this conversation as resolved.
Outdated
}

var catalogPath = Path.Combine(Path.GetTempPath(), "mcpServerCatalog.json");
Comment thread
biswapm marked this conversation as resolved.
File.WriteAllText(catalogPath, responseContent);
return catalogPath;
Comment thread
biswapm marked this conversation as resolved.
}

// Writes the hardcoded V2 catalog when the live V2 endpoint is not yet available.
// Remove once discoverToolServers?api-version=2 is confirmed live (Q1).
public static string WriteHardcodedV2Catalog()
{
var catalogPath = Path.Combine(Path.GetTempPath(), "mcpServerCatalog.json");
File.WriteAllText(catalogPath, McpConstants.V2Catalog.WrappedJson);
return catalogPath;
}
Comment thread
biswapm marked this conversation as resolved.

public static string GetCatalogPath()
{
return Path.Combine(Path.GetTempPath(), "mcpServerCatalog.json");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -188,4 +188,66 @@ public void McpServerConfig_SingleScopeSchema_ShouldDeserializeCorrectly()
Assert.Equal("McpServers.Calendar.All", config.Scope);
Assert.Equal("api://mcp-calendar", config.Audience);
}

[Fact]
public void McpServerConfig_V2Fields_DefaultToNull()
{
// Arrange & Act
var config = new McpServerConfig();

// Assert
Assert.Null(config.Id);
Assert.Null(config.Publisher);
}

[Fact]
public void McpServerConfig_V2JsonDeserialization_PopulatesIdAndPublisher()
{
// Arrange
var json = """
{
"mcpServerName": "mcp_MailTools",
"id": "3fb34f44-7f4e-4e9e-855f-072404166824",
"url": "https://test.agent365.svc.cloud.dev.microsoft/agents/servers/mcp_MailTools",
"scope": "McpServers.Mail.All",
"audience": "05879165-0320-489e-b644-f72b33f3edf0",
"publisher": "Microsoft"
}
""";

// Act
var config = JsonSerializer.Deserialize<McpServerConfig>(json);

// Assert
Assert.NotNull(config);
Assert.Equal("3fb34f44-7f4e-4e9e-855f-072404166824", config.Id);
Assert.Equal("Microsoft", config.Publisher);
Assert.Equal("McpServers.Mail.All", config.Scope);
Assert.Equal("05879165-0320-489e-b644-f72b33f3edf0", config.Audience);
}

[Fact]
public void McpServerConfig_V1JsonDeserialization_LeavesV2FieldsNull()
{
// Arrange — V1 entry has no id or publisher
var json = """
{
"mcpServerName": "mcp_MailTools",
"mcpServerUniqueName": "mcp_MailTools",
"url": "https://agent365.svc.cloud.microsoft/agents/servers/mcp_MailTools",
"scope": "McpServers.Mail.All",
"audience": "ea9ffc3e-8a23-4a7d-836d-234d7c7565c1"
}
""";

// Act
var config = JsonSerializer.Deserialize<McpServerConfig>(json);

// Assert — V2 fields are null, V1 fields are intact
Assert.NotNull(config);
Assert.Null(config.Id);
Assert.Null(config.Publisher);
Assert.Equal("mcp_MailTools", config.McpServerName);
Assert.Equal("ea9ffc3e-8a23-4a7d-836d-234d7c7565c1", config.Audience);
}
}
Loading
Loading