Skip to content

Commit 191f96f

Browse files
authored
Resolves multiple issues when calling azd extension tool (#203)
Resolves multiple issues when azd is invoked from MCP server Adds additional command parameters for cwd, environment and learn to provide additional context and description to the calling agent/LLM . Enables the agent/LLM to learn more about the best practices for azd usage and guidelines in additional to the limited tool description. Returns the azd best practices guide when the learn parameter has been set. Encourages the agent/LLM to run long running operations like provision, deploy, up and down in a terminal instead of within the tool so users get incremental feedback. Today the MCP protocol does not support incremental streaming response feedback that can be handled. At some point in the future when better streaming support is available, we can re-visit this implementation.
1 parent 556378b commit 191f96f

File tree

9 files changed

+249
-23
lines changed

9 files changed

+249
-23
lines changed

.vscode/cspell.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -154,6 +154,7 @@
154154
"LINUXOS",
155155
"LINUXPOOL",
156156
"LINUXVMIMAGE",
157+
"LLM",
157158
"msal",
158159
"mysvc",
159160
"mycluster",

CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,11 @@
55
### Bugs Fixed
66

77
- Fixes Service Bus host name parameter description. https://github.com/Azure/azure-mcp/pull/209/
8+
- Updates the usage patterns of Azure Developer CLI (azd) when invoked from MCP. https://github.com/Azure/azure-mcp/pull/203
9+
10+
### Features Added
11+
12+
### Other Changes
813

914
## 0.0.18 (2025-05-14)
1015

@@ -18,6 +23,7 @@
1823

1924
- Added an opt-in timeout for browser-based authentication to handle cases where the process waits indefinitely if the user closes the browser. https://github.com/Azure/azure-mcp/pull/189
2025

26+
2127
## 0.0.16 (2025-05-13)
2228

2329
### Bugs Fixed

README.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -116,12 +116,13 @@ The Azure MCP Server provides tools for interacting with the following Azure ser
116116
- Support for template discovery, template initialization, provisioning and deployment
117117
- Cross-platform compatibility
118118

119+
Agents and models can discover and learn best practices and usage guidelines for the `azd` MCP tool. For more information, see [AZD Best Practices](https://github.com/Azure/azure-mcp/tree/main/src/Resources/azd-best-practices.txt).
120+
119121
### 🛡️ Azure Best Practices
120122
- Get secure, production-grade Azure SDK best practices for effective code generation.
121123

122124
For detailed command documentation and examples, see [Azure MCP Commands](https://github.com/Azure/azure-mcp/blob/main/docs/azmcp-commands.md).
123125

124-
125126
## 🔌 Getting Started
126127

127128
The Azure MCP Server requires Node.js to install and run the server. If you don't have it installed, follow the instructions [here](https://docs.npmjs.com/downloading-and-installing-node-js-and-npm).

src/Arguments/Extension/AzdArguments.cs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,4 +10,13 @@ public class AzdArguments : GlobalArguments
1010
{
1111
[JsonPropertyName(ArgumentDefinitions.Extension.Azd.CommandName)]
1212
public string? Command { get; set; }
13+
14+
[JsonPropertyName(ArgumentDefinitions.Extension.Azd.CwdName)]
15+
public string? Cwd { get; set; }
16+
17+
[JsonPropertyName(ArgumentDefinitions.Extension.Azd.EnvironmentName)]
18+
public string? Environment { get; set; }
19+
20+
[JsonPropertyName(ArgumentDefinitions.Extension.Azd.LearnName)]
21+
public bool Learn { get; set; }
1322
}

src/AzureMcp.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,7 @@
7979

8080
<ItemGroup>
8181
<EmbeddedResource Include="Resources\azure-best-practices.txt" />
82+
<EmbeddedResource Include="Resources\azd-best-practices.txt" />
8283
</ItemGroup>
8384

8485
<!-- TODO: Remove after 5/19/25 -->

src/Commands/Extension/AzdCommand.cs

Lines changed: 143 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
using System.CommandLine.Parsing;
66
using System.Runtime.InteropServices;
77
using AzureMcp.Arguments.Extension;
8+
using AzureMcp.Helpers;
89
using AzureMcp.Models.Argument;
910
using AzureMcp.Models.Command;
1011
using AzureMcp.Services.Azure;
@@ -19,8 +20,29 @@ public sealed class AzdCommand(ILogger<AzdCommand> logger, int processTimeoutSec
1920
private readonly ILogger<AzdCommand> _logger = logger;
2021
private readonly int _processTimeoutSeconds = processTimeoutSeconds;
2122
private readonly Option<string> _commandOption = ArgumentDefinitions.Extension.Azd.Command.ToOption();
23+
private readonly Option<string> _cwdOption = ArgumentDefinitions.Extension.Azd.Cwd.ToOption();
24+
private readonly Option<string> _environmentOption = ArgumentDefinitions.Extension.Azd.Environment.ToOption();
25+
private readonly Option<bool> _learnOption = ArgumentDefinitions.Extension.Azd.Learn.ToOption();
2226
private static string? _cachedAzdPath;
2327

28+
private readonly IEnumerable<string> longRunningCommands =
29+
[
30+
"provision",
31+
"package",
32+
"deploy",
33+
"up",
34+
"down",
35+
];
36+
37+
private static readonly string _bestPracticesText = LoadBestPracticesText();
38+
39+
private static string LoadBestPracticesText()
40+
{
41+
var assembly = typeof(AzdCommand).Assembly;
42+
const string resourceName = "AzureMcp.Resources.azd-best-practices.txt";
43+
return EmbeddedResourceHelper.ReadEmbeddedResource(assembly, resourceName);
44+
}
45+
2446
private static readonly string[] AzdCliPaths =
2547
[
2648
// Windows
@@ -32,36 +54,77 @@ public sealed class AzdCommand(ILogger<AzdCommand> logger, int processTimeoutSec
3254
protected override string GetCommandName() => "azd";
3355

3456
protected override string GetCommandDescription() =>
35-
"Use this tool to always run all Azure Developer CLI (azd) commands. " +
36-
"Most common activities will be helping users initialize projects, manage environments, provision and deploy infrastructure and apps to their Azure environment. " +
37-
"If an azure.yaml file exists in the workspace, treat it as an existing azd project; otherwise, use init to create one. " +
38-
"Always pass --cwd with the full workspace path, and use -e to specify the environment when required. Confirm with the user before performing destructive operations. " +
39-
"After each command, suggest next steps if available, and ask before proceeding. On errors, prompt for missing info and retry. " +
40-
"Be concise and contextual, using data from the user's environment and workspace to provide accurate, actionable responses. " +
41-
"This tool can create, modify or delete resources in Azure. Always warn and confirm action with the user before performing destructive commands like 'up', 'down', 'provision' or 'deploy'.";
57+
"""
58+
Runs Azure Developer CLI (azd) commands.
59+
Agents and LLM's must always run this tool with the 'learn' parameter and empty 'command' on first use to learn more about 'azd' best practices and usage patterns.
60+
61+
This tool supports the following:
62+
- List, search and show templates to start your project
63+
- Create and initialize new projects and templates
64+
- Show and manage azd configuration
65+
- Show and manage environments and values
66+
- Provision Azure resources
67+
- Deploy applications
68+
- Bring the whole project up and online
69+
- Bring the whole project down and deallocate all Azure resources
70+
- Setup CI/CD pipelines
71+
- Monitor Azure applications
72+
- Show information about the project and its resources
73+
- Show and manage extensions and extension sources
74+
- Show and manage templates and template sources
75+
76+
If unsure about available commands or their parameters, run azd help or azd <group> --help in the command to discover them.
77+
""";
4278

4379
protected override void RegisterOptions(Command command)
4480
{
4581
base.RegisterOptions(command);
4682
command.AddOption(_commandOption);
83+
command.AddOption(_cwdOption);
84+
command.AddOption(_environmentOption);
85+
command.AddOption(_learnOption);
4786
}
4887

4988
protected override void RegisterArguments()
5089
{
5190
base.RegisterArguments();
52-
AddArgument(CreateCommandArgument());
91+
foreach (var arg in CreateArguments())
92+
{
93+
AddArgument(arg);
94+
}
5395
}
5496

55-
private static ArgumentBuilder<AzdArguments> CreateCommandArgument() =>
56-
ArgumentBuilder<AzdArguments>
57-
.Create(ArgumentDefinitions.Extension.Azd.Command.Name, ArgumentDefinitions.Extension.Azd.Command.Description)
58-
.WithValueAccessor(args => args.Command ?? string.Empty)
59-
.WithIsRequired(ArgumentDefinitions.Extension.Azd.Command.Required);
97+
private static ArgumentBuilder<AzdArguments>[] CreateArguments() =>
98+
[
99+
ArgumentBuilder<AzdArguments>
100+
.Create(ArgumentDefinitions.Extension.Azd.Command.Name, ArgumentDefinitions.Extension.Azd.Command.Description)
101+
.WithValueAccessor(args => args.Command ?? string.Empty)
102+
.WithIsRequired(ArgumentDefinitions.Extension.Azd.Command.Required),
103+
104+
ArgumentBuilder<AzdArguments>
105+
.Create(ArgumentDefinitions.Extension.Azd.Cwd.Name, ArgumentDefinitions.Extension.Azd.Cwd.Description)
106+
.WithValueAccessor(args => args.Cwd ?? string.Empty)
107+
.WithIsRequired(ArgumentDefinitions.Extension.Azd.Cwd.Required),
108+
109+
ArgumentBuilder<AzdArguments>
110+
.Create(ArgumentDefinitions.Extension.Azd.Environment.Name, ArgumentDefinitions.Extension.Azd.Environment.Description)
111+
.WithValueAccessor(args => args.Environment ?? string.Empty)
112+
.WithIsRequired(ArgumentDefinitions.Extension.Azd.Environment.Required),
113+
114+
ArgumentBuilder<AzdArguments>
115+
.Create(ArgumentDefinitions.Extension.Azd.Learn.Name, ArgumentDefinitions.Extension.Azd.Learn.Description)
116+
.WithValueAccessor(args => args.Learn.ToString())
117+
.WithIsRequired(ArgumentDefinitions.Extension.Azd.Learn.Required),
118+
];
60119

61120
protected override AzdArguments BindArguments(ParseResult parseResult)
62121
{
63122
var args = base.BindArguments(parseResult);
64123
args.Command = parseResult.GetValueForOption(_commandOption);
124+
args.Cwd = parseResult.GetValueForOption(_cwdOption);
125+
args.Environment = parseResult.GetValueForOption(_environmentOption);
126+
args.Learn = parseResult.GetValueForOption(_learnOption);
127+
65128
return args;
66129
}
67130

@@ -77,8 +140,55 @@ public override async Task<CommandResponse> ExecuteAsync(CommandContext context,
77140
return context.Response;
78141
}
79142

143+
// If the agent is asking for help, return the best practices text
144+
if (args.Learn && string.IsNullOrWhiteSpace(args.Command))
145+
{
146+
context.Response.Message = _bestPracticesText;
147+
context.Response.Status = 200;
148+
return context.Response;
149+
}
150+
80151
ArgumentNullException.ThrowIfNull(args.Command);
152+
ArgumentNullException.ThrowIfNull(args.Cwd);
153+
154+
// Check if the command is a long-running command. The command can contain other flags.
155+
// If is long running command return error message to the user.
156+
if (longRunningCommands.Any(c => args.Command.StartsWith(c, StringComparison.OrdinalIgnoreCase)))
157+
{
158+
var terminalCommand = $"azd {args.Command}";
159+
160+
if (!args.Command.Contains("--cwd", StringComparison.OrdinalIgnoreCase))
161+
{
162+
terminalCommand += $" --cwd {args.Cwd}";
163+
}
164+
if (!args.Command.Contains("-e", StringComparison.OrdinalIgnoreCase) && !string.IsNullOrWhiteSpace(args.Environment))
165+
{
166+
terminalCommand += $" -e {args.Environment}";
167+
}
168+
169+
context.Response.Status = 400;
170+
context.Response.Message =
171+
$"""
172+
The requested command is a long-running command and is better suited to be run in a terminal.
173+
Invoke the following command in a terminal window instead of using this tool so the user can see incremental progress.
174+
175+
```bash
176+
{terminalCommand}
177+
```
178+
""";
179+
180+
return context.Response;
181+
}
182+
81183
var command = args.Command;
184+
185+
command += $" --cwd {args.Cwd}";
186+
187+
if (args.Environment is not null)
188+
{
189+
command += $" -e {args.Environment}";
190+
}
191+
82192
// We need to always pass the --no-prompt flag to avoid prompting for user input and getting the process stuck
83193
command += " --no-prompt";
84194

@@ -245,18 +355,31 @@ private static CommandResponse HandleError(ProcessResult result, CommandResponse
245355
else if (result.Output.Contains("no default response for prompt"))
246356
{
247357
contentResults.Add(
248-
"The command requires user input. Prompt the user for the required information.\n" +
249-
"- If missing Azure subscription use other tools to query and list available subscriptions for the user to select, then set the subscription ID (UUID) in the azd environment.\n" +
250-
"- If missing Azure location, use other tools to query and list available locations for the user to select, then set the location name in the azd environment.\n" +
251-
"- To set values in the azd environment use the command 'azd env set' command."
358+
"""
359+
The command requires user input. Prompt the user for the required information.
360+
- If missing Azure subscription use other tools to query and list available subscriptions for the user to select, then set the subscription ID (UUID) in the azd environment.
361+
- If missing Azure location, use other tools to query and list available locations for the user to select, then set the location name in the azd environment.
362+
- To set values in the azd environment use the command 'azd env set' command."
363+
"""
252364
);
253365
}
254366
else if (result.Output.Contains("user denied delete confirmation"))
255367
{
256368
contentResults.Add(
257-
"The command requires user confirmation to delete resources. Prompt the user for confirmation before proceeding.\n" +
258-
"- If the user confirms, re-run the command with the '--force' flag to bypass the confirmation prompt.\n" +
259-
"- To permanently delete the resources include the '--purge` flag\n"
369+
"""
370+
The command requires user confirmation to delete resources. Prompt the user for confirmation before proceeding.
371+
- If the user confirms, re-run the command with the '--force' flag to bypass the confirmation prompt.
372+
- To permanently delete the resources include the '--purge` flag
373+
"""
374+
);
375+
}
376+
else
377+
{
378+
contentResults.Add(
379+
"""
380+
The command failed. Rerun the command with the '--help' flag to get more information about the command and its parameters.
381+
After reviewing the help information, run the command again with updated parameters.
382+
"""
260383
);
261384
}
262385

src/Models/Argument/ArgumentDefinitions.cs

Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -405,9 +405,44 @@ public static class Azd
405405

406406
public static readonly ArgumentDefinition<string> Command = new(
407407
CommandName,
408-
"The Azure Developer CLI command to execute (without the 'azd' prefix). For example: 'up'.",
408+
"""
409+
The Azure Developer CLI command and arguments to execute (without the 'azd' prefix).
410+
Examples:
411+
- up
412+
- env list
413+
- env get-values
414+
""",
415+
required: false
416+
);
417+
418+
public const string CwdName = "cwd";
419+
420+
public static readonly ArgumentDefinition<string> Cwd = new(
421+
CwdName,
422+
"The current working directory for the command. This is the directory where the command will be executed.",
409423
required: true
410424
);
425+
426+
public const string EnvironmentName = "environment";
427+
public static readonly ArgumentDefinition<string> Environment = new(
428+
EnvironmentName,
429+
"""
430+
The name of the azd environment to use. This is typically the name of the Azure environment (e.g., 'prod', 'dev', 'test', 'staging').
431+
Always set environments for azd commands that support -e, --environment argument.
432+
""",
433+
required: false
434+
);
435+
436+
public const string LearnName = "learn";
437+
public static readonly ArgumentDefinition<bool> Learn = new(
438+
LearnName,
439+
"""
440+
Flag to indicate whether to learn best practices and usage patterns for azd tool.
441+
Always run this command with learn=true and empty command on first run.
442+
""",
443+
defaultValue: false,
444+
required: false
445+
);
411446
}
412447
}
413448

src/Resources/azd-best-practices.txt

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
Azure Developer CLI (azd) Tool — Best Practices and Usage Guidelines
2+
3+
Overview:
4+
- This tool wraps the Azure Developer CLI (`azd`) and can be used to initialize, provision, deploy, configure, and manage Azure-based projects.
5+
- It must always be used with the `cwd` parameter (the absolute path to your project).
6+
- If an environment is set, provide it using `environment`.
7+
8+
Popular commands and categories:
9+
- init: Scaffold a new project from a template (`azd init --template <name>`)
10+
- config: Manage machine/user level settings (`azd config set|unset|show`)
11+
- env: Create, list, and manage environments (`azd env list|new|select|show|get-values`)
12+
- templates: List or search available templates (`azd template list`, `azd template show <name>`)
13+
- up: Provision infrastructure and deploy application code (`azd up`)
14+
- provision: Provision the Azure resources that will host the application (`azd provision`)
15+
- deploy: Deploy application code or containers to already provisioned resources (`azd deploy <service>`)
16+
- pipeline: Configure GitHub Actions CI/CD (`azd pipeline config`)
17+
- auth: Log in to Azure or check auth status (`azd auth login`, `azd auth status`)
18+
- monitor: Launch Azure monitoring dashboards (`azd monitor`)
19+
- package: Build the application into a deployable artifact (`azd package <service>`)
20+
- extensions: Manage installed azd extensions (`azd extension list|install|uninstall|show`)
21+
- extension sources: Manage extension sources/registries (`azd extension source list|add|remove`)
22+
- help: Get detailed help on available command and parameters (`azd help`, `azd <group> --help`)
23+
- version: Check the CLI version
24+
25+
Required Parameters:
26+
- cwd (string): Absolute path to the working directory. Required for all invocations.
27+
- command (string): The azd CLI command to run without `azd` prefix
28+
- environment (string, optional): To specify the environment to use for the command.
29+
- learn (boolean, optional): Set to true to receive this usage guide.
30+
31+
Execution Guidelines:
32+
- Never assume environment names, always prompt the user to supply an environment name that is then used in downstream commands such as `init`.
33+
- If commands require a subscription or location, retrieve configuration defaults from the `config show` command.
34+
- When configuration defaults do not exist, prompt the user to supply a value and leverage other tools to list subscription and locations for easy selection.
35+
- Check for an `azure.yaml` file in the workspace to determine if the project is already initialized.
36+
- Never make assumptions on command names or arguments - Call `help` command or `<command> --help` to contract proper commands.
37+
- When an error occurs, always run help commands to ensure the command exists and is run with the expected parameters and then retry.
38+
- Use `azd help` or `azd <command group> --help` to explore options and flags.
39+
- Commands `provision`, `deploy`, `up` and `down are considered long running operations.
40+
- For long running commands do not use this tool - instead always run them in a terminal so users can see incremental progress passing in the same.
41+
- Other than long running commands, all other azd commands should be executed using the 'azmcp-extension-azd' tool.
42+
- Suggest next steps when provided by previous command output.
43+
- Always prompt the user to confirm before running commands that create, update, or delete Azure resources (e.g. `provision`, `deploy`, `up`, `down`).
44+
45+
Example Commands:
46+
- `command = "template list"`, `cwd = "/workspace"`
47+
- `command = "init --template todo-node"`, `cwd = "/workspace"`, `environmentName = "dev"`
48+
- `command = "pipeline config"`, `cwd = "/workspace"`, `environmentName = "test"`
49+
- `command = "auth login"`, `cwd = "/workspace"`

0 commit comments

Comments
 (0)