Skip to content

feat(agents): add azd ai agent toolbox direct commands#8203

Open
hund030 wants to merge 3 commits into
mainfrom
zhihuan/feat-agent-toolbox-impl
Open

feat(agents): add azd ai agent toolbox direct commands#8203
hund030 wants to merge 3 commits into
mainfrom
zhihuan/feat-agent-toolbox-impl

Conversation

@hund030
Copy link
Copy Markdown
Collaborator

@hund030 hund030 commented May 15, 2026

Summary

Adds the azd ai agent toolbox command group to the azure.ai.agents extension so users can manage versioned, connection-backed tool collections without leaving their terminal. Closes #8143. Implements the design spec from #8160.

Changes

  • internal/cmd/toolbox*.go: six verbs (create, update, delete, show, list, connection {add,remove,list}) and the parent command. create records a local pending entry; the first connection add POSTs v1 and clears it. connection add resolves RemoteTool connections to mcp tool entries and CognitiveSearch (with --index) to azure_ai_search entries; later mutations fetch the default version, edit tools[], POST a new version, then PATCH default_version. Reuses the central resolveProjectEndpoint cascade and the ResolvedEndpoint source enum.
  • internal/cmd/pending_toolboxes.go: per-endpoint pending-toolbox store under extensions.ai-agents.pending-toolboxes.<sha256(endpoint)[:16]> (§ 7); exposed via a pendingToolboxStore interface so tests can substitute.
  • internal/pkg/azure/foundry_toolsets_client.go: extended with ListToolboxes, GetToolboxVersion, ListToolboxVersions, DeleteToolboxVersion, SetDefaultVersion, capped paginator, and Endpoint(). Pipeline helpers (doJSON, toolboxURL, generic listPagedFromClient[T]) collapse the per-method boilerplate.
  • internal/pkg/azure/foundry_projects_client.go: adds GetConnection(ctx, name) (no credentials) for the connection resolver.
  • internal/exterrors/codes.go: new codes (CodeToolboxNotFound, CodeDefaultVersionDelete, CodeOnlyVersionDelete, CodeUnsupportedConnectionCategory, CodeMissingIndex, CodeUnsupportedIndexFlag, CodeDuplicateConnection, CodeConnectionNotFound, CodeConnectionMissingTarget, CodeConnectionNotInToolbox, CodeLastToolRemoval, CodeMissingForceFlag, CodeInvalidToolboxName, CodeMissingUpdateField, CodePendingToolboxStoreFailed, CodeLastToolRemoval) and Op* constants for ServiceFromAzure.
  • Unit tests: table-driven coverage for every verb's branches against a stubbed toolboxClient and connectionResolver, plus the shared helpers (buildToolEntry, filterOutConnection, duplicateConnectionInTools, buildToolboxMcpURL, endpointBucketKey).

@github-actions
Copy link
Copy Markdown

github-actions Bot commented May 15, 2026

📋 Prioritization Note

Thanks for the contribution! The linked issue isn't in the current milestone yet.
Review may take a bit longer — reach out to @rajeshkamal5050 or @kristenwomack if you'd like to discuss prioritization.

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR adds a new azd ai agent toolbox command group to the azure.ai.agents extension to manage Foundry toolboxes as versioned, connection-backed tool collections, including a local “pending toolbox” flow for create prior to the first connection add.

Changes:

  • Introduces toolbox CRUD-ish verbs (create, update, delete, show, list) plus toolbox connection add|remove|list under the agent extension command tree.
  • Adds a per-endpoint pending-toolbox config store (bucketed by a hashed endpoint) and wires it into create, show, list, and promotion on connection add.
  • Extends the Foundry toolbox/projects Azure clients with toolbox pagination/version operations and a connection lookup without credentials, plus unit tests for the new command branches and helper functions.

Reviewed changes

Copilot reviewed 20 out of 20 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
cli/azd/extensions/azure.ai.agents/internal/pkg/azure/foundry_toolsets_client.go Adds shared JSON request helper, toolbox URL builder, cursor-pagination walker, and new toolbox/version operations.
cli/azd/extensions/azure.ai.agents/internal/pkg/azure/foundry_projects_client.go Adds GetConnection (no credentials) for toolbox connection resolution.
cli/azd/extensions/azure.ai.agents/internal/exterrors/codes.go Adds toolbox-specific error codes and Azure service operation names for toolbox flows.
cli/azd/extensions/azure.ai.agents/internal/cmd/root.go Registers the new toolbox command group in the extension root command.
cli/azd/extensions/azure.ai.agents/internal/cmd/toolbox.go Adds toolbox parent command, cross-cutting flags, name/output validation, endpoint resolution, and 404 detection helper.
cli/azd/extensions/azure.ai.agents/internal/cmd/toolbox_client.go Defines a toolboxClient interface to allow unit tests to stub the toolbox API.
cli/azd/extensions/azure.ai.agents/internal/cmd/toolbox_shared.go Adds shared helpers for JSON emission, toolbox-not-found mapping, and tool connection ID walking.
cli/azd/extensions/azure.ai.agents/internal/cmd/toolbox_context.go Adds toolbox/projects client constructors and endpoint parsing helpers.
cli/azd/extensions/azure.ai.agents/internal/cmd/toolbox_create.go Implements toolbox create as a local pending record (no initial POST).
cli/azd/extensions/azure.ai.agents/internal/cmd/toolbox_update.go Implements toolbox update (PATCH default version only).
cli/azd/extensions/azure.ai.agents/internal/cmd/toolbox_delete.go Implements toolbox delete including guarded per-version delete semantics and pending-record clearing.
cli/azd/extensions/azure.ai.agents/internal/cmd/toolbox_show.go Implements toolbox show, including MCP endpoint computation and pending-record rendering.
cli/azd/extensions/azure.ai.agents/internal/cmd/toolbox_list.go Implements toolbox list, merging live toolboxes with local pending entries.
cli/azd/extensions/azure.ai.agents/internal/cmd/toolbox_connection.go Adds toolbox connection add and tool-entry construction logic based on connection category.
cli/azd/extensions/azure.ai.agents/internal/cmd/toolbox_connection_actions.go Implements toolbox connection remove and toolbox connection list.
cli/azd/extensions/azure.ai.agents/internal/cmd/toolbox_connection_resolver.go Adds a resolver that fetches a project connection (no credentials) and maps it into toolbox-ready shape.
cli/azd/extensions/azure.ai.agents/internal/cmd/pending_toolboxes.go Implements per-endpoint pending-toolbox storage and the pendingToolboxStore abstraction.
cli/azd/extensions/azure.ai.agents/internal/cmd/toolbox_test_helpers_test.go Adds mock toolbox client, stub connection resolver, and in-memory pending store for tests.
cli/azd/extensions/azure.ai.agents/internal/cmd/toolbox_helpers_test.go Adds unit tests for helper utilities (validation, tool entry building, filtering, URL building, hashing).
cli/azd/extensions/azure.ai.agents/internal/cmd/toolbox_commands_test.go Adds unit tests covering command branch behavior and key error cases.

Comment thread cli/azd/extensions/azure.ai.agents/internal/cmd/toolbox.go
Comment thread cli/azd/extensions/azure.ai.agents/internal/cmd/toolbox_list.go Outdated
Comment thread cli/azd/extensions/azure.ai.agents/internal/pkg/azure/foundry_toolsets_client.go Outdated
'connection add' against the same toolbox name publishes v1 and clears the
pending record.`,
Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Return an error with suggestion so its clear what to specify. All it says now is:

ERROR: accepts 1 arg(s), received 0

I'd expect it show more like the --help comamnd does:

Usage:
  agent toolbox create <name> [flags]

Foundry requires a non-empty tool list on the first POST, so 'create' does not
contact the service. Instead it records a local pending entry. The first
'connection add' against the same toolbox name publishes v1 and clears the
pending record.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That might be outside of extension control, I think that's coming from Args: cobra.ExactArgs(1) being specified

}
fmt.Printf(
"Registered toolbox %s (pending tools). "+
"Run 'azd ai agent toolbox connection add %s <connection>' to publish v1.\n",
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Improve the formatting for this so its not all on one line - there might be an existing pattern for showing next step guidance and coloring, etc.

If not, then just make each sentence on a new line.


cmd.Flags().StringVar(
&flags.index, "index", "",
"Index name (required when the connection's category is CognitiveSearch).",
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is CognitiveSearch referring to? Need to check if that is the correct branding/service name now.

return nil, exterrors.Validation(
exterrors.CodeUnsupportedConnectionCategory,
fmt.Sprintf(
"connection %q has category %q; v1 supports RemoteTool and CognitiveSearch only",
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't expect this limitation.

}
if promoted {
fmt.Printf(
"Published toolbox %s version %s with connection %s.\n",
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add the toolbox endpoint to the output on a new line so its easily copied by the dev after its published.

A better suggestion, if in an AZD project already, would be to recommend setting an env var to use like:

azd env set <toolboxname>_MCP_ENDPOINT=<endpoint>

But fill in the values for the placeholders.

'connection add' against the same toolbox name publishes v1 and clears the
pending record.`,
Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That might be outside of extension control, I think that's coming from Args: cobra.ExactArgs(1) being specified

CognitiveSearch → azure_ai_search tool (requires --index)
Other categories are rejected.

If the toolbox has a local pending record (from 'toolbox create'), v1 is
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This means that if I want to create a toolbox with 3 tools, I'm going to end up on v3 before I'm done. This feels incredibly painful from a user perspective, as I'm not actually iterating on desired changes, I'm being forced into it. Is there a way this can be done without all these added, unwanted, versions?

if _, err := store.Clear(ctx, endpoint, toolboxName); err != nil {
return exterrors.Internal(
exterrors.CodePendingToolboxStoreFailed,
fmt.Sprintf("failed to clear pending toolbox record: %s", err),
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is the implication here? Is this something the user can recover from? At the very least we should probably indicate that this is a client side only failure, and that the toolbox version was still created correctly.

return exterrors.ServiceFromAzure(err, exterrors.OpGetToolbox)
}

current, err := client.GetToolboxVersion(ctx, toolboxName, tb.DefaultVersion)
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we want a way to add a connection to the non-default toolbox version?

@@ -0,0 +1,281 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a reason that the add command is in one file, and the other commands are in a different file? I'd suggest either a separate file per command, like the toolbox base commands, or everything in one. Could separate out the helper methods if desired to help clean things up.

return exterrors.Validation(
exterrors.CodeMissingForceFlag,
"--no-prompt requires --force on destructive operations",
"add --force to confirm the deletion non-interactively",
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wouldn't this only apply if we're trying to delete the default?

}

// Drop pending records that already exist live-side to avoid duplicates.
liveNames := map[string]struct{}{}
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If the toolbox has been published to the project, we shouldn't have a pending entry for it any more, correct? Is this necessary?

extCtx = ensureExtensionContext(extCtx)
cmd := &cobra.Command{
Use: "list",
Short: "List toolboxes on the project, plus any local pending records.",
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this list all versions, or just the defaults?

Short: "Show a toolbox version, including its computed MCP endpoint.",
Long: `Show a toolbox.

By default shows the default version. Use --version to inspect a specific
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How can a user know how many versions a toolbox has?

Long: `Update a toolbox.

Only --default-version is mutable through PATCH today (§ 4.1). To edit the
description or the tool list, publish a new version with 'connection add' or
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

connection add doesn't let a user change the description, does it?

Copy link
Copy Markdown
Member

@jongio jongio left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Technical findings from code-level analysis. I'm not restating the design/UX feedback from @therealjohn and @trangevi; their reviews cover the broader picture.

Three findings, one medium priority:

  1. [MED] Non-atomic version promotion connection add and connection remove both call CreateToolboxVersion then SetDefaultVersion as two separate API calls. If the second fails, there's an orphaned version that isn't the default, and the error doesn't include the created version number, so recovery via toolbox update --default-version requires the user to first figure out which version got created. Same pattern in toolbox_connection_actions.go around line 120.

  2. [LOW] Silent pagination truncation listPagedFromClient returns partial results without error when the server responds with has_more=true but provides no usable cursor (lines 161-164 of foundry_toolsets_client.go). Unlikely in practice, but a log.Printf warning here would make debugging much easier if it ever happens.

  3. [LOW] No length cap on toolbox/tool names toolboxNamePattern is ^[A-Za-z0-9_-]+$ with no min/max bounds. The existing agent name validation in parse.go enforces 1-63 chars. Worth adding a length cap so users get a clear local error rather than a less helpful service-side 400.


if _, err := client.SetDefaultVersion(ctx, toolboxName, created.Version); err != nil {
return exterrors.ServiceFromAzure(err, exterrors.OpSetDefaultVersion)
}
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If SetDefaultVersion fails here, we've already POSTed a new version to the service. That version exists server-side but isn't the default, and the error doesn't tell the user which version was created.

Two options: (a) include created.Version in the error so users can recover with toolbox update --default-version <v>, or (b) attempt a best-effort DeleteToolboxVersion rollback (with a log warning if that fails too).

Same pattern exists in connection remove (toolbox_connection_actions.go around line 120).

if last == "" {
// HasMore=true with no cursor: bail rather than spin.
return out, nil
}
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When HasMore is true but there's no usable cursor, this silently returns partial results. Callers like ListToolboxes and ListToolboxVersions won't know they got incomplete data. A log.Printf warning here would help debugging, something like pagination: has_more=true but no cursor for %s; returning %d items.

}

// toolboxNamePattern is the validation regex for toolbox and tool names per § 4.2.
var toolboxNamePattern = regexp.MustCompile(`^[A-Za-z0-9_-]+$`)
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This matches any length. The agent name regex in parse.go enforces 1-63 chars. Adding a length cap here would catch overly long names locally instead of letting them hit the server and come back as a less helpful 400.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add azd ai toolbox create/update/show/list/delete (plus connection and tag subcommands) to manage Foundry Toolboxes from any directory

5 participants