@@ -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>
7773public 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
302275callers without the output being too tightly coupled to the tool code.
303276Calls 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
307283A 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
3112871 . Enforce specific fields get set in output
3122881 . Customize error output
313289
314- Tools can also return primitive types, string, ` IEnumerable ` of a supported type, ` Task ` and ` void ` for simple scenarios.
315-
316290All tool response classes must:
317291
3182921 . ** Inherit from ` Response ` ** base class
@@ -364,21 +338,20 @@ public class YourResponseType : Response
364338An 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" )]
368342public 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)
395368catch (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
408380if (errors .Any ())
409381{
410- SetFailure ();
411382 return new YourResponseType
412383 {
413384 ResponseErrors = errors
@@ -459,7 +430,7 @@ dotnet test
459430
460431``` csharp
461432using Azure .Sdk .Tools .Cli .Tests .TestHelpers ;
462- using Azure .Sdk .Tools .Cli .Tools ;
433+ using Azure .Sdk .Tools .Cli .Tools . YourTool ;
463434
464435namespace 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" )]
538509public 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 " ) ]
572540public 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
602570catch (Exception ex )
603571{
604- return new Response { ResponseError = ex .Message }; // Missing SetFailure()
572+ return $" Exception: { ex .Message } " ;
605573}
606574```
0 commit comments