@@ -25,10 +25,15 @@ This document provides a comprehensive guide for implementing commands in Azure
25
25
└── BaseCommand
26
26
└── GlobalCommand<TOptions>
27
27
└── SubscriptionCommand<TOptions>
28
- └── Service-specific base commands
29
- └── Resource-specific commands
30
- ``` - Commands use primary constructors with ILogger injection and optional parameters (e.g., timeouts)
28
+ └── Service-specific base commands (e.g., BaseSqlCommand)
29
+ └── Resource-specific commands (e.g., SqlIndexRecommendCommand)
30
+ ```
31
+
32
+ IMPORTANT:
33
+ - Commands use primary constructors with ILogger injection
31
34
- Classes are always sealed unless explicitly intended for inheritance
35
+ - Commands inheriting from SubscriptionCommand must handle subscription parameters
36
+ - Service-specific base commands should add service-wide options
32
37
- Commands are marked with [McpServerTool] attribute to define their characteristics
33
38
34
39
3. **Command Pattern**
@@ -78,6 +83,12 @@ IMPORTANT:
78
83
- Inherit from appropriate base class (BaseServiceOptions, GlobalOptions, etc.)
79
84
- Never redefine properties from base classes
80
85
- Make properties nullable if not required
86
+ - Use consistent parameter names across services:
87
+ - Use ` subscription ` instead of ` subscriptionId `
88
+ - Use ` resourceGroup ` instead of ` resourceGroupName `
89
+ - Use singular nouns for resource names (e.g., ` server ` not ` serverName ` )
90
+ - Keep parameter names consistent with Azure SDK parameters when possible
91
+ - If services share similar operations (e.g., ListDatabases), use the same parameter order and names
81
92
82
93
### 2. Command Class
83
94
@@ -111,13 +122,13 @@ public sealed class {Resource}{Operation}Command(ILogger<{Resource}{Operation}Co
111
122
112
123
protected override {Resource }{Operation }Options BindOptions (ParseResult parseResult )
113
124
{
114
- var args = base .BindOptions (parseResult );
115
- args .NewOption = parseResult .GetValueForOption (_newOption );
116
- return args ;
125
+ var options = base .BindOptions (parseResult );
126
+ options .NewOption = parseResult .GetValueForOption (_newOption );
127
+ return options ;
117
128
}
118
129
119
130
[McpServerTool (
120
- Destructive = false , // Set to true for commands that modify resources
131
+ Destructive = false , // Set to true for commands that modify resources
121
132
ReadOnly = true , // Set to false for commands that modify resources
122
133
Title = _commandTitle )] // Display name shown in UI
123
134
public override async Task < CommandResponse > ExecuteAsync (CommandContext context , ParseResult parseResult )
@@ -126,7 +137,7 @@ public sealed class {Resource}{Operation}Command(ILogger<{Resource}{Operation}Co
126
137
127
138
try
128
139
{
129
- // Required validation step using the base Validate method
140
+ // Required validation step
130
141
if (! Validate (parseResult .CommandResult , context .Response ).IsValid )
131
142
{
132
143
return context .Response ;
@@ -135,83 +146,112 @@ public sealed class {Resource}{Operation}Command(ILogger<{Resource}{Operation}Co
135
146
// Get the appropriate service from DI
136
147
var service = context .GetService <I {Service }Service >();
137
148
138
- // Call service operation(s)
149
+ // Call service operation(s) with required parameters
139
150
var results = await service .{Operation }(
140
- options .RequiredParam ! ,
141
- options .OptionalParam ,
142
- options .Subscription ! ,
143
- options .Tenant ,
144
- options .RetryPolicy );
151
+ options .RequiredParam ! , // Required parameters end with !
152
+ options .OptionalParam , // Optional parameters are nullable
153
+ options .Subscription ! , // From SubscriptionCommand
154
+ options .RetryPolicy ); // From GlobalCommand
145
155
146
156
// Set results if any were returned
147
157
context .Response .Results = results ? .Count > 0 ?
148
158
ResponseResult .Create (
149
- // Use a strongly-typed result record
150
159
new {Operation }CommandResult (results ),
151
- // Use source generated JsonContext
152
160
{Service }JsonContext .Default .{Operation }CommandResult ) :
153
161
null ;
154
162
}
155
163
catch (Exception ex )
156
164
{
157
- // Log error with context information
158
- _logger .LogError (ex , " Error in {Operation}. Options: {Options}" , Name , args );
159
- // Let base class handle standard error processing
165
+ // Log error with all relevant context
166
+ _logger .LogError (ex ,
167
+ " Error in {Operation}. Required: {Required}, Optional: {Optional}, Options: {@Options}" ,
168
+ Name , options .RequiredParam , options .OptionalParam , options );
160
169
HandleException (context .Response , ex );
161
170
}
162
171
163
172
return context .Response ;
164
173
}
165
174
166
- // Optional: Override HandleException to handle service -specific errors
175
+ // Implementation -specific error handling
167
176
protected override string GetErrorMessage (Exception ex ) => ex switch
168
177
{
169
- // Service-specific errors
170
178
ResourceNotFoundException => " Resource not found. Verify the resource exists and you have access." ,
171
179
AuthorizationException authEx =>
172
180
$" Authorization failed accessing the resource. Details: {authEx .Message }" ,
173
181
ServiceException svcEx => svcEx .Message ,
174
- // Fall back to base handler
175
182
_ => base .GetErrorMessage (ex )
176
183
};
177
184
178
185
protected override int GetStatusCode (Exception ex ) => ex switch
179
186
{
180
- // Map exceptions to HTTP status codes
181
187
ResourceNotFoundException => 404 ,
182
188
AuthorizationException => 403 ,
183
189
ServiceException svcEx => svcEx .Status ,
184
- // Fall back to base handler
185
190
_ => base .GetStatusCode (ex )
186
191
};
187
192
193
+ // Strongly-typed result records
188
194
internal record {Resource }{Operation }CommandResult (List < ResultType > Results );
189
195
}
190
- ```
191
196
192
197
### 3. Base Service Command Classes
193
198
194
199
Each service has its own hierarchy of base command classes that inherit from `GlobalCommand ` or `SubscriptionCommand `. For example :
195
200
196
201
```csharp
202
+ // Copyright (c) Microsoft Corporation.
203
+ // Licensed under the MIT License.
204
+
205
+ using System .Diagnostics .CodeAnalysis ;
206
+ using AzureMcp .Commands .Subscription ;
207
+ using AzureMcp .Models .Option ;
208
+ using AzureMcp .Options .{Service };
209
+ using Azure .Core ;
210
+ using AzureMcp .Models ;
211
+ using Microsoft .Extensions .Logging ;
212
+
213
+ namespace AzureMcp .Commands .{Service};
214
+
197
215
// Base command for all service commands
198
216
public abstract class Base {Service}Command <
199
217
[DynamicallyAccessedMembers (TrimAnnotations .CommandAnnotations )] TOptions >
200
218
: SubscriptionCommand < TOptions > where TOptions : Base {Service }Options , new ()
201
219
{
202
220
protected readonly Option < string > _commonOption = OptionDefinitions .Service .CommonOption ;
203
221
protected readonly Option < string > _resourceGroupOption = OptionDefinitions .Common .ResourceGroup ;
222
+ protected virtual bool RequiresResourceGroup => true ;
204
223
205
224
protected override void RegisterOptions (Command command )
206
225
{
207
226
base .RegisterOptions (command );
208
227
command .AddOption (_commonOption );
228
+
229
+ // Add resource group option if required
230
+ if (RequiresResourceGroup )
231
+ {
232
+ command .AddOption (_resourceGroupOption );
233
+ }
234
+ }
235
+
236
+ protected override TOptions BindOptions (ParseResult parseResult )
237
+ {
238
+ var options = base .BindOptions (parseResult );
239
+ options .CommonOption = parseResult .GetValueForOption (_commonOption );
240
+
241
+ if (RequiresResourceGroup )
242
+ {
243
+ options .ResourceGroup = parseResult .GetValueForOption (_resourceGroupOption );
244
+ }
245
+
246
+ return options ;
209
247
}
210
248
}
211
249
212
250
// Base command for resource-specific commands
213
- public abstract class Base {Resource}Command < TOptions > : Base {Service }Command < TOptions >
214
- where TOptions : Base {Resource }Options , new ()
251
+ public abstract class Base {Resource}Command <
252
+ [DynamicallyAccessedMembers (TrimAnnotations .CommandAnnotations )] TOptions >
253
+ : Base {Service }Command < TOptions >
254
+ where TOptions : Base {Resource }Options , new ()
215
255
{
216
256
protected readonly Option < string > _resourceOption = OptionDefinitions .Service .Resource ;
217
257
@@ -220,6 +260,13 @@ public abstract class Base{Resource}Command<TOptions> : Base{Service}Command<TOp
220
260
base .RegisterOptions (command );
221
261
command .AddOption (_resourceOption );
222
262
}
263
+
264
+ protected override TOptions BindOptions (ParseResult parseResult )
265
+ {
266
+ var options = base .BindOptions (parseResult );
267
+ options .Resource = parseResult .GetValueForOption (_resourceOption );
268
+ return options ;
269
+ }
223
270
}
224
271
```
225
272
0 commit comments