Skip to content

Commit 9f55f17

Browse files
authored
Enhance command documentation and implementation guidelines for Azure MCP (#330)
1 parent f33eced commit 9f55f17

File tree

1 file changed

+74
-27
lines changed

1 file changed

+74
-27
lines changed

src/Docs/new-command.md

Lines changed: 74 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -25,10 +25,15 @@ This document provides a comprehensive guide for implementing commands in Azure
2525
└── BaseCommand
2626
└── GlobalCommand<TOptions>
2727
└── 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
3134
- 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
3237
- Commands are marked with [McpServerTool] attribute to define their characteristics
3338
3439
3. **Command Pattern**
@@ -78,6 +83,12 @@ IMPORTANT:
7883
- Inherit from appropriate base class (BaseServiceOptions, GlobalOptions, etc.)
7984
- Never redefine properties from base classes
8085
- 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
8192

8293
### 2. Command Class
8394

@@ -111,13 +122,13 @@ public sealed class {Resource}{Operation}Command(ILogger<{Resource}{Operation}Co
111122

112123
protected override {Resource}{Operation}Options BindOptions(ParseResult parseResult)
113124
{
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;
117128
}
118129

119130
[McpServerTool(
120-
Destructive = false, // Set to true for commands that modify resources
131+
Destructive = false, // Set to true for commands that modify resources
121132
ReadOnly = true, // Set to false for commands that modify resources
122133
Title = _commandTitle)] // Display name shown in UI
123134
public override async Task<CommandResponse> ExecuteAsync(CommandContext context, ParseResult parseResult)
@@ -126,7 +137,7 @@ public sealed class {Resource}{Operation}Command(ILogger<{Resource}{Operation}Co
126137

127138
try
128139
{
129-
// Required validation step using the base Validate method
140+
// Required validation step
130141
if (!Validate(parseResult.CommandResult, context.Response).IsValid)
131142
{
132143
return context.Response;
@@ -135,83 +146,112 @@ public sealed class {Resource}{Operation}Command(ILogger<{Resource}{Operation}Co
135146
// Get the appropriate service from DI
136147
var service = context.GetService<I{Service}Service>();
137148

138-
// Call service operation(s)
149+
// Call service operation(s) with required parameters
139150
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
145155
146156
// Set results if any were returned
147157
context.Response.Results = results?.Count > 0 ?
148158
ResponseResult.Create(
149-
// Use a strongly-typed result record
150159
new {Operation}CommandResult(results),
151-
// Use source generated JsonContext
152160
{Service}JsonContext.Default.{Operation}CommandResult) :
153161
null;
154162
}
155163
catch (Exception ex)
156164
{
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);
160169
HandleException(context.Response, ex);
161170
}
162171

163172
return context.Response;
164173
}
165174

166-
// Optional: Override HandleException to handle service-specific errors
175+
// Implementation-specific error handling
167176
protected override string GetErrorMessage(Exception ex) => ex switch
168177
{
169-
// Service-specific errors
170178
ResourceNotFoundException => "Resource not found. Verify the resource exists and you have access.",
171179
AuthorizationException authEx =>
172180
$"Authorization failed accessing the resource. Details: {authEx.Message}",
173181
ServiceException svcEx => svcEx.Message,
174-
// Fall back to base handler
175182
_ => base.GetErrorMessage(ex)
176183
};
177184

178185
protected override int GetStatusCode(Exception ex) => ex switch
179186
{
180-
// Map exceptions to HTTP status codes
181187
ResourceNotFoundException => 404,
182188
AuthorizationException => 403,
183189
ServiceException svcEx => svcEx.Status,
184-
// Fall back to base handler
185190
_ => base.GetStatusCode(ex)
186191
};
187192

193+
// Strongly-typed result records
188194
internal record {Resource}{Operation}CommandResult(List<ResultType> Results);
189195
}
190-
```
191196

192197
### 3. Base Service Command Classes
193198

194199
Each service has its own hierarchy of base command classes that inherit from `GlobalCommand` or `SubscriptionCommand`. For example:
195200

196201
```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+
197215
// Base command for all service commands
198216
public abstract class Base{Service}Command<
199217
[DynamicallyAccessedMembers(TrimAnnotations.CommandAnnotations)] TOptions>
200218
: SubscriptionCommand<TOptions> where TOptions : Base{Service}Options, new()
201219
{
202220
protected readonly Option<string> _commonOption = OptionDefinitions.Service.CommonOption;
203221
protected readonly Option<string> _resourceGroupOption = OptionDefinitions.Common.ResourceGroup;
222+
protected virtual bool RequiresResourceGroup => true;
204223

205224
protected override void RegisterOptions(Command command)
206225
{
207226
base.RegisterOptions(command);
208227
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;
209247
}
210248
}
211249

212250
// 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()
215255
{
216256
protected readonly Option<string> _resourceOption = OptionDefinitions.Service.Resource;
217257

@@ -220,6 +260,13 @@ public abstract class Base{Resource}Command<TOptions> : Base{Service}Command<TOp
220260
base.RegisterOptions(command);
221261
command.AddOption(_resourceOption);
222262
}
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+
}
223270
}
224271
```
225272

0 commit comments

Comments
 (0)