|
| 1 | +# aaz-dev-mcp |
| 2 | + |
| 3 | +A [Model Context Protocol](https://modelcontextprotocol.io) server that exposes |
| 4 | +[aaz-dev-tools](https://github.com/Azure/aaz-dev-tools) workflows as tools an |
| 5 | +LLM agent can call. It wraps the same Python controllers the web UI uses, so |
| 6 | +generated CLI code is identical to what you'd get by clicking through the |
| 7 | +browser. |
| 8 | + |
| 9 | +## Scope (v1) |
| 10 | + |
| 11 | +Reproduce the end-to-end flow of an Azure CLI swagger-version bump PR (e.g. |
| 12 | +[azure-cli#33222](https://github.com/Azure/azure-cli/pull/33222)): |
| 13 | + |
| 14 | +1. Configure repo paths. |
| 15 | +2. Create or load a workspace. |
| 16 | +3. Add swagger resources at a specific API version. |
| 17 | +4. (Optional) tweak help text on command groups. |
| 18 | +5. Generate to the `aaz` repo. |
| 19 | +6. Select per-command versions for an azure-cli module and regenerate Python code. |
| 20 | + |
| 21 | +## Install |
| 22 | + |
| 23 | +This package depends on `aaz-dev-tools` itself being installed in the same |
| 24 | +Python environment. |
| 25 | + |
| 26 | +```bash |
| 27 | +# from the repo root |
| 28 | +python -m venv .venv |
| 29 | +source .venv/bin/activate |
| 30 | +pip install -e . # installs aaz-dev-tools controllers |
| 31 | +pip install -e src/aaz_dev_mcp # installs aaz-dev-mcp + MCP SDK |
| 32 | +``` |
| 33 | + |
| 34 | +## Configuration |
| 35 | + |
| 36 | +The server reads the same environment variables aaz-dev-tools uses: |
| 37 | + |
| 38 | +| Variable | Purpose | |
| 39 | +| --- | --- | |
| 40 | +| `AAZ_PATH` | clone of `Azure/aaz` | |
| 41 | +| `AAZ_SWAGGER_PATH` | clone of `Azure/azure-rest-api-specs` | |
| 42 | +| `AAZ_CLI_PATH` | clone of `Azure/azure-cli` | |
| 43 | +| `AAZ_CLI_EXTENSION_PATH` | clone of `Azure/azure-cli-extensions` | |
| 44 | +| `AAZ_DEV_WORKSPACE_FOLDER` | where workspace ws.json files live (default `~/.aaz_dev/workspaces` — same default as the Flask UI, so workspaces are shared by default) | |
| 45 | + |
| 46 | +You can also override at runtime by calling the `configure` tool first. |
| 47 | + |
| 48 | +## Running |
| 49 | + |
| 50 | +The server speaks MCP over **stdio**: |
| 51 | + |
| 52 | +```bash |
| 53 | +aaz-dev-mcp |
| 54 | +``` |
| 55 | + |
| 56 | +### Claude Desktop / OpenCode config snippet |
| 57 | + |
| 58 | +```json |
| 59 | +{ |
| 60 | + "mcpServers": { |
| 61 | + "aaz-dev": { |
| 62 | + "command": "/absolute/path/to/.venv/bin/aaz-dev-mcp", |
| 63 | + "env": { |
| 64 | + "AAZ_PATH": "/Users/you/workspaces/aaz", |
| 65 | + "AAZ_SWAGGER_PATH": "/Users/you/workspaces/azure-rest-api-specs", |
| 66 | + "AAZ_CLI_PATH": "/Users/you/workspaces/azure-cli", |
| 67 | + "AAZ_CLI_EXTENSION_PATH": "/Users/you/workspaces/azure-cli-extensions" |
| 68 | + } |
| 69 | + } |
| 70 | + } |
| 71 | +} |
| 72 | +``` |
| 73 | + |
| 74 | +## Sharing workspaces with the Flask UI |
| 75 | + |
| 76 | +Workspaces are not MCP-specific. Both the MCP server and the Flask UI in |
| 77 | +`aaz-dev` go through the same `WorkspaceManager`, which stores each |
| 78 | +workspace as `<AAZ_DEV_WORKSPACE_FOLDER>/<name>/{ws.json, Resources/...}` |
| 79 | +on disk. |
| 80 | + |
| 81 | +If both processes use the same `AAZ_DEV_WORKSPACE_FOLDER` (the default |
| 82 | +`~/.aaz_dev/workspaces` for both), an MCP-created workspace shows up in |
| 83 | +the UI's workspace list automatically and you can edit its command tree, |
| 84 | +arguments, examples, etc., in the browser. Likewise, the MCP can `load` |
| 85 | +and modify workspaces that were created in the UI. |
| 86 | + |
| 87 | +A few things to keep in mind: |
| 88 | + |
| 89 | +- **No IPC** — if you have the UI open while the MCP writes the same |
| 90 | + workspace (or vice versa), the UI's in-memory copy is stale. Refresh. |
| 91 | +- **Optimistic concurrency** — `WorkspaceManager.save()` compares a |
| 92 | + `ws.version` UTC timestamp against the on-disk copy |
| 93 | + (`workspace_manager.py:213`). Concurrent saves to the same workspace |
| 94 | + fail with `ResourceConflict("Workspace Changed after: ...")` rather |
| 95 | + than corrupting state. Re-load and retry. |
| 96 | +- **Other paths must also match** — if the UI's `AAZ_PATH` / |
| 97 | + `AAZ_CLI_PATH` / etc. point at different checkouts than the MCP's, |
| 98 | + `generate_to_aaz` and `update_*_module` from each side write to |
| 99 | + different repos. Keep them aligned (or be intentional about the split, |
| 100 | + e.g. UI -> real repos, MCP -> worktrees). |
| 101 | + |
| 102 | +## Tools |
| 103 | + |
| 104 | +| Tool | Purpose | |
| 105 | +| --- | --- | |
| 106 | +| `configure` | Set/inspect paths to aaz, swagger, cli, extensions, workspace folder. | |
| 107 | +| `list_workspaces` | List ws.json folders under the configured workspace folder. | |
| 108 | +| `create_workspace` | Create a new workspace. Errors if it already exists. | |
| 109 | +| `load_workspace` | Load an existing workspace's command tree. | |
| 110 | +| `add_swagger_resources` | Add ARM resource paths at a given API version into the workspace. | |
| 111 | +| `set_node_help` | Update help text / stage on a command-group node. | |
| 112 | +| `set_command_help` | Update help text / stage on a leaf command. | |
| 113 | +| `rename_command_group` | Rename or re-parent a command-group node, e.g. `node_names=["consumption","usage-detail"]` → `new_node_names=["consumption","usage"]`. | |
| 114 | +| `rename_command` | Rename or re-parent a leaf command, e.g. `leaf_names=["consumption","pricesheet","default","show"]` → `new_leaf_names=["consumption","pricesheet","show"]`. | |
| 115 | +| `update_argument` | Patch a single argument on a leaf (options/help/stage/hide/group/singular_options). Use to add aliases like `--name`/`-n` to `--budget-name`. Pass `clear_group=True` to ungroup an arg. | |
| 116 | +| `flatten_argument` | Flatten an object argument into its sub-arguments (e.g. `--time-period` → `--start-date` + `--end-date`). | |
| 117 | +| `unflatten_argument` | Inverse of `flatten_argument`. | |
| 118 | +| `list_command_examples` | Read the current examples on a leaf command. | |
| 119 | +| `add_command_example` | Append a single manual `{name, commands}` example (mirrors the editor's "Add Example" dialog). | |
| 120 | +| `add_examples_from_swagger` | Generate examples from the OpenAPI spec (the editor's "By OpenAPI Specification" button) and persist them; supports `replace=False/True`. | |
| 121 | +| `set_command_examples` | Replace the full example list on a leaf (pass `[]` to clear). | |
| 122 | +| `generate_to_aaz` | Export the workspace command tree to the `aaz` repo. | |
| 123 | +| `get_main_module` / `get_extension_module` | Read the current CLI profile for an azure-cli or extensions module. | |
| 124 | +| `update_main_module` / `update_extension_module` | Generate CLI code from a full profiles dict. Accepts `by_patch` (default `true`). | |
| 125 | +| `select_command_versions` | High-level: pick `{command: version}` and generate. Accepts `by_patch` (default `true`). | |
| 126 | + |
| 127 | +## Typical workflow |
| 128 | + |
| 129 | +1. `configure(...)` (or rely on env vars) |
| 130 | +2. `create_workspace(name="consumption-2024-08-01", mod_names="consumption", resource_provider="Microsoft.Consumption")` |
| 131 | +3. `add_swagger_resources(name=..., module="consumption", version="2024-08-01", resource_paths=[...])` |
| 132 | +4. `rename_command_group(name=..., node_names=["consumption","usage-detail"], new_node_names=["consumption","usage"])` (and similar for `reservation-summary` → `reservation summary`, `reservation-detail` → `reservation detail`; `rename_command` for `pricesheet default show` → `pricesheet show`). |
| 133 | +5. `set_node_help(name=..., node_names=["consumption","budget"], help={"short": "Manage consumption budgets."})` (optional) |
| 134 | +6. `generate_to_aaz(name=...)` |
| 135 | +7. `select_command_versions(target="main", module_name="consumption", by_patch=False, command_versions={"consumption budget create": "2024-08-01", ...})` |
| 136 | + |
| 137 | +### `by_patch` semantics |
| 138 | + |
| 139 | +`by_patch` (passed to `update_*_module` / `select_command_versions`) only |
| 140 | +controls whether unmodified commands re-read their existing config files. |
| 141 | +It does **not** preserve commands that are absent from the input profile — |
| 142 | +codegen always rewrites the entire `aaz/latest/` tree. |
| 143 | + |
| 144 | +If you are introducing brand-new leaves (e.g. `consumption usage list`, |
| 145 | +`consumption reservation summary list`) that don't yet exist in the CLI |
| 146 | +module, you must pass `by_patch=False`. Otherwise the profile generator |
| 147 | +hits `AssertionError: command.cfg is not None` |
| 148 | +(`az_profile_generator.py:103`). |
| 149 | + |
| 150 | +## Notes |
| 151 | + |
| 152 | +- Each tool catches `ValueError` from `Config` setters and returns it as a |
| 153 | + structured error. |
| 154 | +- Errors raised by controllers (`exceptions.ResourceConflict`, |
| 155 | + `exceptions.InvalidAPIUsage`, etc.) propagate as MCP tool errors. |
| 156 | +- Workspaces use optimistic concurrency: a `ws.version` timestamp is checked |
| 157 | + on every save. If you run the Flask dev server and the MCP simultaneously |
| 158 | + and both write the same workspace, the second writer will error. |
0 commit comments