Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"context"
"fmt"

"azureaiagent/internal/exterrors"
"azureaiagent/internal/pkg/agents/agent_api"

"github.com/Azure/azure-sdk-for-go/sdk/azcore"
Expand Down Expand Up @@ -87,10 +88,11 @@ func resolveAgentEndpoint(ctx context.Context, accountName string, projectName s
Key: "AZURE_AI_PROJECT_ENDPOINT",
})
if err != nil || envValue.Value == "" {
return "", fmt.Errorf(
"AZURE_AI_PROJECT_ENDPOINT not found in azd environment '%s'\n\n"+
"Provide --account-name and --project-name flags, "+
"or run 'azd ai agent init' to configure the endpoint", envResponse.Environment.Name)
return "", exterrors.Dependency(
exterrors.CodeMissingAiProjectEndpoint,
fmt.Sprintf("AZURE_AI_PROJECT_ENDPOINT not found in azd environment '%s'", envResponse.Environment.Name),
"run 'azd provision' to provision your Azure resources and set the endpoint",
)
}

return envValue.Value, nil
Expand Down
64 changes: 42 additions & 22 deletions cli/azd/extensions/azure.ai.agents/internal/cmd/init.go
Original file line number Diff line number Diff line change
Expand Up @@ -1120,35 +1120,54 @@ func (a *InitAction) addToProject(ctx context.Context, targetDir string, agentMa
var agentConfig = project.ServiceTargetAgentConfig{}

resourceDetails := []project.Resource{}
toolboxDetails := []project.Toolbox{}
switch agentDef.Kind {
case agent_yaml.AgentKindHosted:
// Handle tool resources that require connection names
// Handle tool resources that require connection names or toolbox dependencies
if agentManifest.Resources != nil {
for _, resource := range agentManifest.Resources {
// Try to cast to ToolResource
if toolResource, ok := resource.(agent_yaml.ToolResource); ok {
// Check if this is a resource that requires a connection name
if toolResource.Id == "bing_grounding" || toolResource.Id == "azure_ai_search" {
// Prompt the user for a connection name
resp, err := a.azdClient.Prompt().Prompt(ctx, &azdext.PromptRequest{
Options: &azdext.PromptOptions{
Message: fmt.Sprintf("Enter a connection name for adding the resource %s to your Microsoft Foundry project", toolResource.Id),
IgnoreHintKeys: true,
DefaultValue: toolResource.Id,
},
})
if err != nil {
return fmt.Errorf("prompting for connection name for %s: %w", toolResource.Id, err)
}
switch res := resource.(type) {
case agent_yaml.ToolResource:
// Prompt the user for a connection name
resp, err := a.azdClient.Prompt().Prompt(ctx, &azdext.PromptRequest{
Options: &azdext.PromptOptions{
Message: fmt.Sprintf("Enter a connection name for adding the resource %s to your Microsoft Foundry project", res.Id),
IgnoreHintKeys: true,
DefaultValue: res.Id,
},
})
if err != nil {
return fmt.Errorf("prompting for connection name for %s: %w", res.Id, err)
}

// Add to resource details
resourceDetails = append(resourceDetails, project.Resource{
Resource: toolResource.Id,
ConnectionName: resp.Value,
})
resourceDetails = append(resourceDetails, project.Resource{
Resource: res.Id,
ConnectionName: resp.Value,
})

case agent_yaml.ToolboxResource:
toolbox := project.Toolbox{
Name: res.Id,
}

if res.Options != nil {
if desc, ok := res.Options["description"].(string); ok {
toolbox.Description = desc
}
if tools, ok := res.Options["tools"].([]any); ok {
for _, t := range tools {
toolJSON, err := json.Marshal(t)
if err != nil {
return fmt.Errorf("failed to marshal tool definition for toolbox %s: %w",
res.Id, err)
}
toolbox.Tools = append(toolbox.Tools, toolJSON)
}
}
}

toolboxDetails = append(toolboxDetails, toolbox)
}
// Skip the resource if the cast fails
}
}

Expand All @@ -1162,6 +1181,7 @@ func (a *InitAction) addToProject(ctx context.Context, targetDir string, agentMa

agentConfig.Deployments = a.deploymentDetails
agentConfig.Resources = resourceDetails
agentConfig.Toolboxes = toolboxDetails

// Detect startup command from the project source directory
startupCmd, err := resolveStartupCommandForInit(ctx, a.azdClient, a.projectConfig.Path, targetDir, a.flags.NoPrompt)
Expand Down
32 changes: 32 additions & 0 deletions cli/azd/extensions/azure.ai.agents/internal/cmd/listen.go
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,12 @@ func envUpdate(ctx context.Context, azdClient *azdext.AzdClient, azdProject *azd
}
}

if len(foundryAgentConfig.Toolboxes) > 0 {
if err := toolboxEnvUpdate(ctx, foundryAgentConfig.Toolboxes, azdClient, currentEnvResponse.Environment.Name); err != nil {
return err
}
}

return nil
}

Expand Down Expand Up @@ -219,6 +225,32 @@ func resourcesEnvUpdate(ctx context.Context, resources []project.Resource, azdCl
return setEnvVar(ctx, azdClient, envName, "AI_PROJECT_DEPENDENT_RESOURCES", escapedJsonString)
}

func toolboxEnvUpdate(ctx context.Context, toolboxes []project.Toolbox, azdClient *azdext.AzdClient, envName string) error {
// Resolve the project endpoint to construct MCP URLs
envResp, err := azdClient.Environment().GetValue(ctx, &azdext.GetEnvRequest{
EnvName: envName,
Key: "AZURE_AI_PROJECT_ENDPOINT",
})
if err != nil {
return fmt.Errorf("failed to get AZURE_AI_PROJECT_ENDPOINT: %w", err)
}

projectEndpoint := envResp.Value
if projectEndpoint == "" {
return fmt.Errorf("AZURE_AI_PROJECT_ENDPOINT not set; required to resolve toolbox MCP endpoints")
}

for _, tb := range toolboxes {
mcpEndpoint := project.ToolboxMcpEndpoint(projectEndpoint, tb.Name)
envVar := project.ToolboxNameToEnvVar(tb.Name) + "_MCP_ENDPOINT"
if err := setEnvVar(ctx, azdClient, envName, envVar, mcpEndpoint); err != nil {
return err
}
}

return nil
}

func containerAgentHandling(ctx context.Context, azdClient *azdext.AzdClient, project *azdext.ProjectConfig, svc *azdext.ServiceConfig) error {
servicePath := svc.RelativePath
fullPath := filepath.Join(project.Path, servicePath)
Expand Down
1 change: 1 addition & 0 deletions cli/azd/extensions/azure.ai.agents/internal/cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@ func NewRootCommand() *cobra.Command {
rootCmd.AddCommand(newShowCommand())
rootCmd.AddCommand(newMonitorCommand())
rootCmd.AddCommand(newFilesCommand())
rootCmd.AddCommand(newToolboxCommand())

return rootCmd
}
26 changes: 26 additions & 0 deletions cli/azd/extensions/azure.ai.agents/internal/cmd/toolbox.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

package cmd

import (
"github.com/spf13/cobra"
)

func newToolboxCommand() *cobra.Command {
cmd := &cobra.Command{
Use: "toolbox",
Short: "Manage Foundry toolboxes.",
Long: `Manage Foundry toolboxes in the current Azure AI Foundry project.

Toolboxes are named collections of tools (MCP servers, OpenAPI endpoints, first-party tools)
exposed through a unified MCP-compatible endpoint with platform-managed auth.`,
}

cmd.AddCommand(newToolboxListCommand())
cmd.AddCommand(newToolboxShowCommand())
cmd.AddCommand(newToolboxCreateCommand())
cmd.AddCommand(newToolboxDeleteCommand())

return cmd
}
182 changes: 182 additions & 0 deletions cli/azd/extensions/azure.ai.agents/internal/cmd/toolbox_create.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,182 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

package cmd

import (
"encoding/json"
"errors"
"fmt"
"os"

"azureaiagent/internal/exterrors"
"azureaiagent/internal/pkg/agents/agent_api"
"azureaiagent/internal/project"

"github.com/Azure/azure-sdk-for-go/sdk/azcore"
"github.com/azure/azure-dev/cli/azd/pkg/azdext"
"github.com/azure/azure-dev/cli/azd/pkg/output"
"github.com/spf13/cobra"
)

func newToolboxCreateCommand() *cobra.Command {
cmd := &cobra.Command{
Use: "create <path-to-toolbox.json>",
Short: "Create a toolbox in the Foundry project.",
Long: `Create a new toolset from a JSON payload file.

The payload file must contain a JSON object with at least "name" and "tools" fields.
If a toolbox with the same name already exists, you will be prompted to confirm
before overwriting (use --no-prompt to auto-confirm).`,
Example: ` # Create a toolbox from a JSON file
azd ai agent toolbox create toolbox.json

# Create with auto-confirm for scripting
azd ai agent toolbox create toolbox.json --no-prompt`,
Args: cobra.MaximumNArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
ctx := azdext.WithAccessToken(cmd.Context())
setupDebugLogging(cmd.Flags())

if len(args) == 0 {
return exterrors.Validation(
exterrors.CodeInvalidToolboxPayload,
"missing required payload file path",
"Provide a path to a toolbox JSON file, for example:\n"+
" azd ai agent toolbox create path/to/toolbox.json",
)
}

payloadPath := args[0]

// Read and parse the payload file
data, err := os.ReadFile(payloadPath) //nolint:gosec // G304: path is from user CLI arg, validated below
if err != nil {
return exterrors.Validation(
exterrors.CodeInvalidToolboxPayload,
fmt.Sprintf("failed to read payload file '%s': %s", payloadPath, err),
"Check that the file path is correct and the file is readable",
)
}

var createReq agent_api.CreateToolboxRequest
if err := json.Unmarshal(data, &createReq); err != nil {
return exterrors.Validation(
exterrors.CodeInvalidToolboxPayload,
fmt.Sprintf("failed to parse payload file '%s': %s", payloadPath, err),
"Ensure the file contains valid JSON with 'name' and 'tools' fields",
)
}

if createReq.Name == "" {
return exterrors.Validation(
exterrors.CodeInvalidToolboxPayload,
"toolbox payload is missing required 'name' field",
"Add a 'name' field to the JSON payload",
)
}
if len(createReq.Tools) == 0 {
return exterrors.Validation(
exterrors.CodeInvalidToolboxPayload,
"toolbox payload is missing required 'tools' field or tools array is empty",
"Add a 'tools' array with at least one tool definition",
)
}

endpoint, err := resolveAgentEndpoint(ctx, "", "")
if err != nil {
return err
}

credential, err := newAgentCredential()
if err != nil {
return exterrors.Auth(
exterrors.CodeCredentialCreationFailed,
fmt.Sprintf("failed to create credential: %s", err),
"Run 'azd auth login' to authenticate",
)
}

client := agent_api.NewAgentClient(endpoint, credential)

// Check if toolbox already exists
existing, err := client.GetToolbox(ctx, createReq.Name, agent_api.ToolboxAPIVersion)
if err == nil && existing != nil {
// Toolset exists — prompt for overwrite confirmation
if !rootFlags.NoPrompt {
azdClient, azdErr := azdext.NewAzdClient()
if azdErr != nil {
return fmt.Errorf("failed to create azd client for prompting: %w", azdErr)
}
defer azdClient.Close()

resp, promptErr := azdClient.Prompt().Confirm(ctx, &azdext.ConfirmRequest{
Options: &azdext.ConfirmOptions{
Message: fmt.Sprintf(
"Toolbox '%s' already exists with %d tool(s). Overwrite?",
existing.Name, len(existing.Tools),
),
},
})
if promptErr != nil {
if exterrors.IsCancellation(promptErr) {
return exterrors.Cancelled("toolbox creation cancelled")
}
return fmt.Errorf("failed to prompt for confirmation: %w", promptErr)
}
if !*resp.Value {
fmt.Println("toolbox creation cancelled.")
return nil
}
}

// Update the existing toolbox
updateReq := &agent_api.UpdateToolboxRequest{
Description: createReq.Description,
Metadata: createReq.Metadata,
Tools: createReq.Tools,
}

toolbox, updateErr := client.UpdateToolbox(ctx, createReq.Name, updateReq, agent_api.ToolboxAPIVersion)
if updateErr != nil {
return exterrors.ServiceFromAzure(updateErr, exterrors.OpUpdateToolbox)
}

mcpEndpoint := project.ToolboxMcpEndpoint(endpoint, toolbox.Name)
fmt.Printf("Toolbox '%s' updated successfully (%d tool(s)).\n", toolbox.Name, len(toolbox.Tools))
fmt.Printf("MCP Endpoint: %s\n", mcpEndpoint)
printMcpEnvTip(toolbox.Name, mcpEndpoint)
return nil
}

// Check if the error is a 404 (not found) — proceed with create
var respErr *azcore.ResponseError
if err != nil && !(errors.As(err, &respErr) && respErr.StatusCode == 404) {
return exterrors.ServiceFromAzure(err, exterrors.OpGetToolbox)
}

// Create new toolbox
toolbox, createErr := client.CreateToolbox(ctx, &createReq, agent_api.ToolboxAPIVersion)
if createErr != nil {
return exterrors.ServiceFromAzure(createErr, exterrors.OpCreateToolbox)
}

mcpEndpoint := project.ToolboxMcpEndpoint(endpoint, toolbox.Name)
fmt.Printf("Toolbox '%s' created successfully (%d tool(s)).\n", toolbox.Name, len(toolbox.Tools))
fmt.Printf("MCP Endpoint: %s\n", mcpEndpoint)
printMcpEnvTip(toolbox.Name, mcpEndpoint)
return nil
},
}

return cmd
}

func printMcpEnvTip(toolboxName, mcpEndpoint string) {
envVar := project.ToolboxNameToEnvVar(toolboxName) + "_MCP_ENDPOINT"
fmt.Println()
fmt.Println(output.WithHintFormat(
"Hint: Store the endpoint in your azd environment so your agent code can reference it:"))
fmt.Printf(" %s\n", output.WithHighLightFormat(
"azd env set %s %s", envVar, mcpEndpoint))
}
Loading
Loading