Skip to content

Commit 5ff78f2

Browse files
authored
Update docs in line with class changes (#12166)
1 parent 74adbe5 commit 5ff78f2

1 file changed

Lines changed: 37 additions & 69 deletions

File tree

tools/azsdk-cli/docs/new-tool.md

Lines changed: 37 additions & 69 deletions
Original file line numberDiff line numberDiff line change
@@ -67,13 +67,9 @@ All tools in the azsdk-cli project follow a consistent architecture:
6767
**Command Hierarchy Examples:**
6868
```csharp
6969
// Single group: azsdk log <sub-command>
70-
CommandHierarchy = [ SharedCommandGroups.Log ];
70+
public override CommandGroup[] CommandHierarchy { get; set; } = [ SharedCommandGroups.Log ];
7171

7272
// Multiple groups: azsdk eng cleanup <sub-command>
73-
CommandHierarchy = [ SharedCommandGroups.EngSys, SharedCommandGroups.Cleanup ];
74-
75-
// If using a primary constructor
76-
// Multiple groups: azsdk example demo <sub-command>
7773
public override CommandGroup[] CommandHierarchy { get; set; } = [
7874
SharedCommandGroups.Example,
7975
SharedCommandGroups.Demo
@@ -101,6 +97,7 @@ public override CommandGroup[] CommandHierarchy { get; set; } = [
10197

10298
**Common Dependencies:**
10399
- `ILogger<YourTool>` - Always required for logging
100+
- `IProcessHelper` - Helper for running external processes
104101
- `IAzureService` - For Azure authentication and credentials
105102
- `IDevOpsService` - For Azure DevOps operations
106103
- Custom service interfaces for your tool's specific needs
@@ -151,37 +148,26 @@ public class YourTool(
151148
private readonly Option<bool> flagOption = new(["--flag", "-f"], () => false, "Boolean flag description");
152149

153150
// CLI Command Configuration
154-
protected override Command GetCommand()
155-
{
156-
var command = new Command("your-command", "Description for CLI help");
157-
158-
command.AddArgument(requiredArg);
159-
command.AddOption(optionalParam);
160-
command.AddOption(flagOption);
161-
162-
command.SetHandler(async ctx => { await HandleCommand(ctx, ctx.GetCancellationToken()); });
163-
164-
return command;
165-
}
151+
protected override Command GetCommand() =>
152+
new("your-command", "Description for CLI help")
153+
{
154+
requiredArg, optionalParam, flagOption
155+
};
166156

167157
// CLI Command Handler
168-
public override async Task HandleCommand(InvocationContext ctx, CancellationToken ct)
158+
public override async Task<CommandResponse> HandleCommand(InvocationContext ctx, CancellationToken ct)
169159
{
170160
// Extract parameters from CLI context
171161
var input = ctx.ParseResult.GetValueForArgument(requiredArg);
172162
var param = ctx.ParseResult.GetValueForOption(optionalParam);
173163
var flag = ctx.ParseResult.GetValueForOption(flagOption);
174164

175165
// Call your main logic (can be shared with MCP methods)
176-
var result = await ProcessRequest(input, param, flag, ct);
177-
178-
// Set exit code and output result
179-
ctx.ExitCode = ExitCode;
180-
output.Output(result);
166+
return await ProcessRequest(input, param, flag, ct);
181167
}
182168

183169
// MCP Server Method - exposed to LLM agents
184-
[McpServerTool(Name = "your_tool_method"), Description("Description for LLM agents")]
170+
[McpServerTool(Name = "azsdk_your_tool_method"), Description("Description for LLM agents")]
185171
public async Task<YourResponseType> ProcessRequest(string input, string? optionalParam = null, CancellationToken ct = default)
186172
{
187173
try
@@ -191,7 +177,6 @@ public class YourTool(
191177
catch (Exception ex)
192178
{
193179
logger.LogError(ex, "Error processing MCP request: {input}", input);
194-
SetFailure();
195180
return new YourResponseType
196181
{
197182
ResponseError = $"Error processing request: {ex.Message}"
@@ -221,48 +206,36 @@ public class ComplexTool(
221206
private readonly Option<string> fooOption = new(["--foo"], "Foo") { IsRequired = true };
222207
private readonly Option<string> barOption = new(["--bar"], "Bar");
223208

224-
protected override List<Command> GetCommands()
225-
{
226-
List<Commands> subCommands = [
227-
new(SubCommandName1, "Analyze something", { fooOption })
228-
new(SubCommandName2, "Process something", { fooOption, barOption })
229-
];
230-
231-
SetHandler(subCommands, async ctx => { await HandleCommand(ctx, ctx.GetCancellationToken()); });
232-
return subCommands;
233-
}
209+
protected override List<Command> GetCommands() => [
210+
new(SubCommandName1, "Analyze something", { fooOption }),
211+
new(SubCommandName2, "Process something", { fooOption, barOption })
212+
];
234213

235-
public override async Task HandleCommand(InvocationContext ctx, CancellationToken ct)
214+
public override async Task<CommandResponse> HandleCommand(InvocationContext ctx, CancellationToken ct)
236215
{
237216
var commandName = ctx.ParseResult.CommandResult.Command.Name;
238217

239218
if (commandName == SubCommandName1)
240219
{
241220
var foo = ctx.ParseResult.GetValueForOption(fooOption);
242-
var result1 = await SubCommand1(foo, ct);
243-
ctx.ExitCode = ExitCode;
244-
output.Output(result1);
245-
return;
221+
return await SubCommand1(foo, ct);
246222
}
247223

248224
if (commandName == SubCommandName2)
249225
{
250226
var foo = ctx.ParseResult.GetValueForOption(fooOption);
251227
var bar = ctx.ParseResult.GetValueForOption(barOption);
252-
var result2 = await SubCommand2(foo, bar, ct);
253-
ctx.ExitCode = ExitCode;
254-
output.Output(result2);
255-
return;
228+
return await SubCommand2(foo, bar, ct);
256229
}
257230
}
258231

259-
[McpServerTool(Name = "sub_command_1"), Description("Handles first stuff")]
232+
[McpServerTool(Name = "azsdk_sub_command_1"), Description("Handles first stuff")]
260233
public async Task<DefaultCommandResponse> SubCommand1(string foo, CancellationToken ct)
261234
{
262235
// Implementation
263236
}
264237

265-
[McpServerTool(Name = "sub_command_2"), Description("Handles second stuff")]
238+
[McpServerTool(Name = "azsdk_sub_command_2"), Description("Handles second stuff")]
266239
public async Task<YourResponseType> SubCommand2(string foo, string bar, CancellationToken ct)
267240
{
268241
// Implementation
@@ -302,6 +275,9 @@ Response handling strategies were created with the intent to flexibly handle mul
302275
callers without the output being too tightly coupled to the tool code.
303276
Calls could be from a CLI invocation in the terminal, tool calls from an MCP client, and potentially more.
304277

278+
NOTE: In CLI mode, the `ResponseError` or `ResponseErrors` property being set on a response object will
279+
default `ExitCode` to `1`. The `ExitCode` property can also be manually overridden.
280+
305281
### Response Class Requirements
306282

307283
A custom response class is not always necessary. It should be defined when the tool needs to:
@@ -311,8 +287,6 @@ A custom response class is not always necessary. It should be defined when the t
311287
1. Enforce specific fields get set in output
312288
1. Customize error output
313289

314-
Tools can also return primitive types, string, `IEnumerable` of a supported type, `Task` and `void` for simple scenarios.
315-
316290
All tool response classes must:
317291

318292
1. **Inherit from `Response`** base class
@@ -364,21 +338,20 @@ public class YourResponseType : Response
364338
An example usage of `DefaultCommandResponse`:
365339

366340
```csharp
367-
[McpServerTool(Name = "hello-world"), Description("Echoes the message back to the client")]
341+
[McpServerTool(Name = "azsdk_hello_world"), Description("Echoes the message back to the client")]
368342
public DefaultCommandResponse EchoSuccess(string message)
369343
{
370344
try
371345
{
372346
logger.LogInformation("Echoing message: {message}", message);
373347
return new()
374348
{
375-
Result = $"RESPONDING TO '{message}' with SUCCESS: {ExitCode}"
349+
Result = $"RESPONDING TO '{message}' with SUCCESS"
376350
};
377351
}
378352
catch (Exception ex)
379353
{
380354
logger.LogError(ex, "Error occurred while echoing message: {message}", message);
381-
SetFailure();
382355
return new()
383356
{
384357
ResponseError = $"Error occurred while processing '{message}': {ex.Message}"
@@ -395,7 +368,6 @@ public DefaultCommandResponse EchoSuccess(string message)
395368
catch (Exception ex)
396369
{
397370
logger.LogError(ex, "Error processing {input}", input);
398-
SetFailure();
399371
return new YourResponseType
400372
{
401373
ResponseError = $"Failed to process {input}: {ex.Message}"
@@ -407,7 +379,6 @@ var errors = new List<string>();
407379
// ... collect errors
408380
if (errors.Any())
409381
{
410-
SetFailure();
411382
return new YourResponseType
412383
{
413384
ResponseErrors = errors
@@ -459,7 +430,7 @@ dotnet test
459430

460431
```csharp
461432
using Azure.Sdk.Tools.Cli.Tests.TestHelpers;
462-
using Azure.Sdk.Tools.Cli.Tools;
433+
using Azure.Sdk.Tools.Cli.Tools.YourTool;
463434

464435
namespace Azure.Sdk.Tools.Cli.Tests;
465436

@@ -469,11 +440,11 @@ internal class YourToolTests
469440
public async Task YourTool_ProcessInput_ReturnsExpectedResult()
470441
{
471442
var tool = new YourTool(new TestLogger<YourTool>());
472-
473443
var result = await tool.YourToolMethod("test-input");
474444

475445
Assert.That(result.ResponseError, Is.Null);
476446
Assert.That(result.Result, Is.Not.Null);
447+
Assert.That(result.ToString(), Is.EqualTo("test response"));
477448
}
478449
}
479450
```
@@ -484,7 +455,6 @@ internal class YourToolTests
484455
- **Always wrap top-level methods** in try/catch blocks
485456
- **Use specific exception types** when possible for better error messages
486457
- **Log errors with context** before returning error responses
487-
- **Call `SetFailure()`** on tool instance for all error cases
488458

489459
### 2. Logging
490460
- **Use ILogger for all logging** - never `Console.WriteLine` or similar
@@ -502,9 +472,10 @@ internal class YourToolTests
502472
- **Provide meaningful ToString() implementations** in response classes for CLI output
503473

504474
### 4. MCP Server Integration
505-
- **Use descriptive MCP method names** (snake_case: `analyze_pipeline`)
475+
- **Use descriptive MCP tool names** (snake_case: `azsdk_analyze_pipeline`)
506476
- **Provide clear descriptions** for LLM agents
507477
- **Design parameters for LLM consumption** - clear names and simple parameter types. Be wary of optional parameters that the LLM might eagerly come up with values for.
478+
- **Tool names must be prefixed with azsdk_**
508479

509480
### 5. Command Design Guidelines
510481

@@ -523,7 +494,7 @@ internal class YourToolTests
523494

524495
### 7. Namespace and Organization Rules
525496

526-
- **Correct namespace**: `Azure.Sdk.Tools.Cli.Tools`
497+
- **Correct namespace**: `Azure.Sdk.Tools.Cli.Tools.[YourToolCategory]`
527498
- **File organization**: Group related tools in sub-directories, but keep flat namespace
528499
- **Tool registration**: Always add to `SharedOptions.ToolsList`
529500
- **Dependency patterns**: Use constructor injection, avoid static dependencies
@@ -534,7 +505,7 @@ internal class YourToolTests
534505

535506
```csharp
536507
// Good: Proper error handling
537-
[McpServerTool, Description("Processes data")]
508+
[McpServerTool(Name = "azsdk_process_data"), Description("Processes data")]
538509
public async Task<ProcessResponse> ProcessData(string input, CancellationToken ct = default)
539510
{
540511
try
@@ -546,7 +517,6 @@ public async Task<ProcessResponse> ProcessData(string input, CancellationToken c
546517
catch (Exception ex)
547518
{
548519
logger.LogError(ex, "Failed to process data: {input}", input);
549-
SetFailure();
550520
return new ProcessResponse
551521
{
552522
ResponseError = $"Failed to process data: {ex.Message}"
@@ -555,20 +525,18 @@ public async Task<ProcessResponse> ProcessData(string input, CancellationToken c
555525
}
556526

557527
// Good: Shared logic between CLI and MCP
558-
public override async Task HandleCommand(InvocationContext ctx, CancellationToken ct)
528+
public override async Task<CommandResponse> HandleCommand(InvocationContext ctx, CancellationToken ct)
559529
{
560530
var input = ctx.ParseResult.GetValueForArgument(inputArg);
561-
var result = await ProcessData(input, ct);
562-
ctx.ExitCode = ExitCode;
563-
output.Output(result);
531+
return await ProcessData(input, ct);
564532
}
565533
```
566534

567535
### ❌ Anti-patterns to Avoid
568536

569537
```csharp
570-
// Bad: No error handling
571-
[McpServerTool]
538+
// Bad: No try/catch error handling
539+
[McpServerTool(Name = "azsdk_process_data")]
572540
public ProcessResponse ProcessData(string input)
573541
{
574542
var result = DoWork(input); // Can throw exceptions
@@ -587,7 +555,7 @@ public class BadTool : MCPTool
587555
}
588556

589557
// Bad: Wrong namespace
590-
namespace Azure.Sdk.Tools.Cli.Tools.YourTool // Incorrect
558+
namespace Azure.Sdk.Tools.Cli.Tools // Incorrect, should be Azure.Sdk.Tools.Cli.Tools.YourToolCategory
591559
{
592560
public class YourTool : MCPTool { }
593561
}
@@ -598,9 +566,9 @@ public void DoWork()
598566
Console.WriteLine("Working..."); // Use ILogger instead
599567
}
600568

601-
// Bad: Not calling SetFailure on errors
569+
// Bad: Not returning a response object with ResponseError set
602570
catch (Exception ex)
603571
{
604-
return new Response { ResponseError = ex.Message }; // Missing SetFailure()
572+
return $"Exception: {ex.Message}";
605573
}
606574
```

0 commit comments

Comments
 (0)