fix: add MCP V2 audience support and prevent removal by setup blueprint - #373
Conversation
- McpConstants: add V2ScopeValue, IsV1Scope(), ResolveAudienceOrAtgFallback() to centralize audience normalization (null/api://-prefix/"default" → ATG AppId) - ManifestHelper: add GetScopesByAudienceAsync() returning scopes grouped by resolved audience (resourceAppId) — V1 entries map to ATG, V2 GUID entries get their own key - PermissionsSubcommand.ConfigureMcpPermissionsAsync: replace single ATG ResourcePermissionSpec with per-audience specs from GetScopesByAudienceAsync, fixing V2 inheritable-permission configuration in 'setup permissions mcp' - PermissionsSubcommand.RemoveStaleCustomPermissionsAsync: extend protectedIds with all audience IDs from the manifest so re-running 'setup blueprint' no longer removes V2 inheritable permissions set by 'setup permissions mcp' - AddPermissionsSubcommand: when reading from manifest, use GetScopesByAudienceAsync and call AddRequiredResourceAccessAsync per audience (V1 + V2); explicit --scopes path unchanged (still targets ATG)
There was a problem hiding this comment.
Pull request overview
Adds MCP V2 (per-audience AppId) support across manifest parsing and permission configuration so V2 inheritable permissions aren’t removed when setup blueprint is re-run, and so develop add-permissions can add requiredResourceAccess per audience.
Changes:
- Introduces manifest scope grouping by resolved audience (resourceAppId) via
ManifestHelper.GetScopesByAudienceAsync. - Updates
setup permissions mcpanddevelop add-permissionsto apply permissions per audience (V1 + V2), and protects manifest audiences from stale-permission cleanup. - Adds MCP audience normalization helpers/constants in
McpConstants.
Reviewed changes
Copilot reviewed 4 out of 4 changed files in this pull request and generated 6 comments.
| File | Description |
|---|---|
| src/Microsoft.Agents.A365.DevTools.Cli/Helpers/ManifestHelper.cs | Adds GetScopesByAudienceAsync to group manifest scopes by resolved audience. |
| src/Microsoft.Agents.A365.DevTools.Cli/Constants/McpConstants.cs | Adds V2 scope constant and audience/scope helpers used by manifest parsing. |
| src/Microsoft.Agents.A365.DevTools.Cli/Commands/SetupSubcommands/PermissionsSubcommand.cs | Configures MCP permissions per audience and protects manifest audiences from stale custom-permission removal. |
| src/Microsoft.Agents.A365.DevTools.Cli/Commands/DevelopSubcommands/AddPermissionsSubcommand.cs | Adds requiredResourceAccess per audience when reading from manifest; retains single-resource behavior for explicit --scopes. |
Resolved conflicts in PermissionsSubcommand.cs (kept both: migration branch --remove-legacy-scopes feature AND V2 audience protection in RemoveStaleCustomPermissionsAsync). Removed duplicate declarations of V2ScopeValue/IsV1Scope/ResolveAudienceOrAtgFallback in McpConstants.cs and duplicate GetScopesByAudienceAsync in ManifestHelper.cs introduced by auto-merge.
Dependency Review✅ No vulnerabilities or license issues or OpenSSF Scorecard issues found.Scanned FilesNone |
…Async GetScopesByAudienceAsync always seeds McpServersMetadata.Read.All under the ATG key when excludeLegacyAtg=false, so Count==0 was never reachable.
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 2 out of 2 changed files in this pull request and generated 1 comment.
Comments suppressed due to low confidence (1)
src/Microsoft.Agents.A365.DevTools.Cli/Commands/SetupSubcommands/PermissionsSubcommand.cs:447
GetScopesByAudienceAsync()normalizes legacy/unset audiences toMcpConstants.WorkIQToolsProdAppId, but this method now uses the returned keys directly asResourcePermissionSpec.ResourceAppId. If the CLI is configured to use a non-default Agent365 Tools AppId viaConfigConstants.GetAgent365ToolsResourceAppId(setupConfig.Environment)(e.g., env var override), V1/legacy manifest entries would be configured against the wrong resource app id. Consider re-keying/merging the ATG entry fromWorkIQToolsProdAppIdto the environment-resolved ATG app id before buildingspecs.
var manifestPath = Path.Combine(setupConfig.DeploymentProjectPath ?? string.Empty, McpConstants.ToolingManifestFileName);
var scopesByAudience = await ManifestHelper.GetScopesByAudienceAsync(
manifestPath, excludeLegacyAtg: removeLegacyAtgScopes);
// Validate all scopes are known: V1 pattern, V2 value, or metadata scope
var unknownScopes = scopesByAudience.Values
.SelectMany(s => s)
.Where(s =>
!McpConstants.IsV1Scope(s) &&
!string.Equals(s, McpConstants.V2ScopeValue, StringComparison.OrdinalIgnoreCase) &&
!string.Equals(s, "McpServersMetadata.Read.All", StringComparison.OrdinalIgnoreCase))
.Distinct(StringComparer.OrdinalIgnoreCase)
.ToList();
if (unknownScopes.Count > 0)
{
foreach (var unknownScope in unknownScopes)
logger.LogError("Unknown scope '{Scope}'. Re-run: a365 develop add-mcp-servers.", unknownScope);
return false;
}
var specs = scopesByAudience
.Select(kvp => new ResourcePermissionSpec(
kvp.Key, "Agent 365 Tools", kvp.Value, SetInheritable: true))
.ToList();
…ation ResolveAudienceOrAtgFallback and GetScopesByAudienceAsync hard-coded WorkIQToolsProdAppId, causing permissions to target the wrong resource app when A365_MCP_APP_ID_* env var overrides are active. - Add ResolveAudienceOrAtgFallback(audience, atgAppId) overload; existing no-arg overload delegates to it (backward compatible). - Add resolvedAtgAppId param to GetScopesByAudienceAsync and GetServerNamesByAudienceAsync (default null = prod constant). - All callers with setupConfig.Environment now pass ConfigConstants.GetAgent365ToolsResourceAppId(environment) so the resolved value flows through manifest parsing and audience grouping.
…s new non-DW setup path Resolved conflicts in 3 files: - AddPermissionsSubcommand: kept V2 per-audience GetScopesByAudienceAsync logic; applied main's cosmetic log message style (removed [SUCCESS]/[FAILED]/[ERROR] prefixes) - AdminSubcommand: took main's --blueprint-id config-free path + agent instance registration; removed old inline spec-building (now delegated to BuildConfiguredPermissionSpecsAsync) - AllSubcommand: took main's refactored step-method structure; preserved resolvedAtgAppId fix in BuildPermissionSpecsAsync Also applied the resolvedAtgAppId fix to SetupHelpers.BuildConfiguredPermissionSpecsAsync so AdminSubcommand's config-dir path uses the correct environment-resolved ATG app ID.
|
Copilot resolve the merge conflicts in this pull request |
Merge conflicts are resolved on this PR branch in commit |
Reconciles the Teams Graph migration with main's Azure App Service infra removal (#379), non-DW --agent-name flow (#365), managerApplications attribute (#372), MCP V2 audience support (#373), and related changes. Key resolutions: - BotConfigurator.cs modify/delete: kept the delete; main's changes to it (making Location optional in the ABS payload) don't apply to Teams Graph. - --m365 opt-in flag, TeamsGraphBackendConfigurator, SkippedDueToRollout, and rollout-cutoff date preserved intact. - ExecuteAllCleanupAsync no longer takes a configurator; --agent-name /--tenant-id/--yes flags from main and their bootstrap config path preserved. - UpdateEndpointAsync kept in simplified clear-then-set form; obsolete ABS regression test (Step 1/Step 1.5 pattern) removed. - SetupHelpers.RegisterBlueprintMessagingEndpointAsync kept in its simplified form returning EndpointRegistrationResult; endpoint-name length check dropped. - SetupContext renamed to use backendConfigurator; matching test-only callers updated. Tests: 1316 passed, 0 failed, 13 skipped. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Summary
RemoveStaleCustomPermissionsAsynchad a hard-codedprotectedIdslist that only covered V1/known resource app IDs. V2 MCP audience GUIDs (set bysetup permissions mcp/setup all) were treated as stale custom permissions and deleted wheneversetup blueprintre-ran.develop add-permissions: only issued a singleAddRequiredResourceAccessAsynccall against the shared ATG app ID, ignoring per-server V2 audience GUIDs entirely.Changes
McpConstants: addV2ScopeValue,IsV1Scope(),ResolveAudienceOrAtgFallback()— centralizes audience normalization (null /api://-prefixed /"default"→ ATG AppId; everything else is a V2 GUID)ManifestHelper: addGetScopesByAudienceAsync()returningDictionary<string, string[]>(resourceAppId → scopes); V1 entries resolve to ATG, V2 GUID entries get their own keyPermissionsSubcommand.ConfigureMcpPermissionsAsync: replace single ATGResourcePermissionSpecwith per-audience specs fromGetScopesByAudienceAsync, fixing V2 inheritable-permission configuration insetup permissions mcpPermissionsSubcommand.RemoveStaleCustomPermissionsAsync: extendprotectedIdswith all audience IDs from the manifest — re-runningsetup blueprintno longer removes V2 inheritable permissionsAddPermissionsSubcommand: when reading from manifest, useGetScopesByAudienceAsyncand callAddRequiredResourceAccessAsyncper audience (V1 + V2); explicit--scopespath unchangedBackward compatibility
RemoveStaleCustomPermissionsAsyncis idempotent;BatchPermissionsOrchestratorskips already-configured permissionssetup blueprintflow is unchanged — no new steps, no new auth promptsTest plan
setup permissions mcpwith a V2 manifest, verify V2 audiences are configured as inheritable permissionssetup blueprint, verify V2 inheritable permissions are preserveddevelop add-permissionswith a V2 manifest, verify per-audiencerequiredResourceAccessentries are added