Introduce the new options pattern#2637
Conversation
1da22c4 to
1a5225c
Compare
| public class SubscriptionListOptions : GlobalOptions; | ||
| public class SubscriptionListOptions : SubscriptionOptions | ||
| { | ||
| public string? ResourceGroup { get; set; } |
There was a problem hiding this comment.
Doesn't make sense for this to continue having this option
There was a problem hiding this comment.
true, but I didn't want to affect the user-facing api in this PR. All options are preserved with their existing "requiredness", name and description
|
|
||
| public AuthMethod? AuthMethod { get; set; } | ||
|
|
||
| [Option(Name = "retry")] |
There was a problem hiding this comment.
What happens to the Name here given that this is annotated on a model which has another set of values?
There was a problem hiding this comment.
The name becomes a prefix to the properties of the RetrtyPolicyOptions class.
There was a problem hiding this comment.
And the parent object in the inputSchema would be retry: {}? I don't think that would be clear to the LLM. Maybe we can have Name override the property name and a different annotation for the prefix on child properties. Also, would the current prefix apply only to direct children or nested ones too?
2b56c66 to
534b462
Compare
| using NSubstitute; | ||
| using Xunit; | ||
|
|
||
| using Microsoft.Mcp.Core.Areas.Server.Models; |
There was a problem hiding this comment.
Seems to be in an odd spot
| if (options.Tool != null && options.Tool.Length > 0) | ||
| { | ||
| options.Mode = ModeTypes.All; | ||
| } |
There was a problem hiding this comment.
Well, this is really interesting behavior... doesn't seem right for another option to implicitly override another like this. Not an issue in this PR but something that probably should be discussed as a follow up.
| if (hasNamespace && hasTool) | ||
| { | ||
| commandResult.AddError("The --namespace and --tool options cannot be used together. Please specify either --namespace to filter by service namespace or --tool to filter by specific tool names, but not both."); | ||
| validationResult.AddError("The --namespace and --tool options cannot be used together. Please specify either --namespace to filter by service namespace or --tool to filter by specific tool names, but not both."); |
There was a problem hiding this comment.
Comment for tracking, not an issue in this PR.
Using --namespace, etc. ties the error message to a CLI-like mode. MCP parameters don't use -- prefixes and would just be namespace here.
Can we write an issue to clean this up so that our error messages return based on the mode used (CLI or MCP right now). In CLI we use --namespace ... options and in MCP we use namespace ... parameters.
| /// </summary> | ||
| [JsonPropertyName("mode")] | ||
| public string? Mode { get; set; } = ModeTypes.NamespaceProxy; | ||
| public string? Mode { get; set; } |
There was a problem hiding this comment.
Another thing noticed due to changes here but isn't due to the PR.
We should make an enum for Mode instead of is being a string
534b462 to
d74dad9
Compare
|
|
||
| return (int)response.Status; | ||
| } | ||
| catch (CommandValidationException ex) |
There was a problem hiding this comment.
When does this get thrown here? It seems that BaseCommand should handle catching CommandValidationException and using HandleException to process it by having BindOptions and ValidateOptions in the same block.
|
|
||
| public List<string> Errors { get; } = []; | ||
|
|
||
| public void AddError(string error) => Errors.Add(error); |
There was a problem hiding this comment.
If we're adding this, should we make Errors a private field instead of a property?
| private static readonly DiagnosticDescriptor Rule = new( | ||
| DiagnosticId, | ||
| title: "Optional group contains required member", | ||
| messageFormat: "Nullable complex property '{0}' contains non-nullable member '{1}'. All properties within a nullable complex type must be nullable.", |
There was a problem hiding this comment.
Does this need to handle chains more deeply nested than one level? For example, Foo.Bar.Baz where Foo is nullable, Bar is nullable, but Baz is non-null.
There was a problem hiding this comment.
Maybe this checks recursively?
| while (current is not null) | ||
| { | ||
| if (current.IsGenericType && | ||
| current.Name == "BaseCommand" && |
There was a problem hiding this comment.
Can be a follow-up but it also seems that we should have a rule where a command never implements IBaseCommand directly, as the current state of this PR that wouldn't function properly.
Or the design in this PR needs to be changed so that BaseCommand doesn't contain common implementation needed by all command.
| public string Type { get; set; } = "string"; | ||
|
|
||
| [JsonPropertyName("required")] | ||
| [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] |
There was a problem hiding this comment.
Should we be ignoring this in JSON serialization when the value is false?
| /// Resolves the subscription from the parse result, falling back to Azure CLI profile | ||
| /// or AZURE_SUBSCRIPTION_ID environment variable. |
There was a problem hiding this comment.
This really should be the docstring for the default implementation, not the interface. The interface should allow the implementation to do whatever it wants.
| /// <summary> | ||
| /// A description of what the option controls. Used in help text and by AI agents. | ||
| /// </summary> | ||
| public string? Description { get; init; } |
There was a problem hiding this comment.
I thought the plan was for Description to be non-null
| PropertyInfo[] allProperties = type.GetProperties(BindingFlags.Public | BindingFlags.Instance); | ||
|
|
||
| // When a derived class hides a base property with 'new', GetProperties returns both. | ||
| // Keep only the most-derived version (the one whose DeclaringType is closest to 'type'). |
There was a problem hiding this comment.
Is this a behavior handled by how type.GetProperties works where it checks super types first for their properties? If not, the check below seems wrong as it just checks IsAssignableFrom without checking depth.
7b6a3f0 to
b64f194
Compare
What does this PR do?
This pull request introduces significant refactoring to the Azure MCP command and options infrastructure, primarily to simplify option binding and validation, and to improve consistency across commands. The changes also include updates to retry policy configuration, test adjustments, and new project references.
GitHub issue number?
[Link to the GitHub issue this PR addresses]Pre-merge Checklist
servers/Azure.Mcp.Server/README.mdand/orservers/Fabric.Mcp.Server/README.mddocumentationREADME.mdchanges running the script./eng/scripts/Process-PackageReadMe.ps1. See Package READMEToolDescriptionEvaluatorand obtained a score of0.4or more and a top 3 ranking for all related test promptsconsolidated-tools.jsonbreaking-changelabelservers/Azure.Mcp.Server/docs/azmcp-commands.md./eng/scripts/Update-AzCommandsMetadata.ps1to update tool metadata inazmcp-commands.md(required for CI)servers/Azure.Mcp.Server/docs/e2eTestPrompts.mdcrypto mining, spam, data exfiltration, etc.)/azp run mcp - pullrequest - liveto run Live Test Pipeline