Skip to content
Merged
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 @@ -451,7 +451,7 @@ func TestBuildToolEntry_RejectsInvalidName(t *testing.T) {
Category: connections.ConnectionTypeRemoteTool,
Name: "tools.v1", // dot is not in ^[A-Za-z0-9_-]+$
Target: "https://mcp",
}, "")
}, "", "")
le := requireLocalError(t, err, exterrors.CodeInvalidToolboxName)
assert.Contains(t, le.Message, "tool entry name")
assert.Contains(t, le.Message, "tools.v1")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,10 @@ func newToolboxConnectionCommand(extCtx *azdext.ExtensionContext) *cobra.Command
Short: "Manage the connection-backed tools attached to a toolbox.",
Long: `Manage the connection-backed tools attached to a toolbox.

Tools are project connections (MCP servers via RemoteTool, or Azure AI Search
indexes via CognitiveSearch). Each mutation publishes a new immutable version
and retargets the toolbox default.`,
Tools are project connections. Supported categories: RemoteTool (MCP),
CognitiveSearch (Azure AI Search), RemoteA2A, and GroundingWithCustomSearch.
Each mutation publishes a new immutable version and retargets the toolbox
default.`,
}
cmd.AddCommand(newToolboxConnectionAddCommand(extCtx))
cmd.AddCommand(newToolboxConnectionRemoveCommand(extCtx))
Expand All @@ -33,24 +34,43 @@ and retargets the toolbox default.`,
}

// buildToolEntry returns the tool-entry map appropriate for the connection's
// category. Enforces the --index flag rules and the `tool.name` regex.
func buildToolEntry(conn *projectConnection, index string) (map[string]any, error) {
// category. Enforces per-input flag rules (--index, --instance-name) and the
// `tool.name` regex.
func buildToolEntry(conn *projectConnection, index, instanceName string) (map[string]any, error) {
if err := validateToolName(conn.Name); err != nil {
return nil, err
}
// Normalize whitespace-only inputs up front so cross-category flag
// rejection and required-input validation agree (e.g. `--index " "`
// should not be treated as "user supplied a value").
index = strings.TrimSpace(index)
instanceName = strings.TrimSpace(instanceName)
// --index is only meaningful for CognitiveSearch; reject elsewhere.
if index != "" && conn.Category != connections.ConnectionTypeCognitiveSearch {
return nil, exterrors.Validation(
exterrors.CodeUnsupportedIndexFlag,
fmt.Sprintf(
"--index is only valid for CognitiveSearch connections, "+
"connection %q has category %q",
conn.Name, conn.Category,
),
"omit --index for non-CognitiveSearch connections",
)
}
// --instance-name is only meaningful for GroundingWithCustomSearch.
if instanceName != "" && conn.Category != connections.ConnectionTypeGroundingWithCustomSearch {
return nil, exterrors.Validation(
exterrors.CodeUnsupportedInstanceNameFlag,
fmt.Sprintf(
"--instance-name is only valid for GroundingWithCustomSearch connections, "+
"connection %q has category %q",
conn.Name, conn.Category,
),
"omit --instance-name for non-GroundingWithCustomSearch connections",
)
}
Comment thread
hund030 marked this conversation as resolved.
switch conn.Category {
case connections.ConnectionTypeRemoteTool:
if index != "" {
return nil, exterrors.Validation(
exterrors.CodeUnsupportedIndexFlag,
fmt.Sprintf(
"--index is only valid for CognitiveSearch connections, "+
"connection %q has category %q",
conn.Name, conn.Category,
),
"omit --index for RemoteTool (MCP) connections",
)
}
// Reject locally rather than letting the service produce a generic 400.
if strings.TrimSpace(conn.Target) == "" {
return nil, exterrors.Validation(
Expand All @@ -71,7 +91,7 @@ func buildToolEntry(conn *projectConnection, index string) (map[string]any, erro
}, nil

case connections.ConnectionTypeCognitiveSearch:
if strings.TrimSpace(index) == "" {
if index == "" {
return nil, exterrors.Validation(
exterrors.CodeMissingIndex,
fmt.Sprintf(
Expand All @@ -94,15 +114,44 @@ func buildToolEntry(conn *projectConnection, index string) (map[string]any, erro
},
}, nil

case connections.ConnectionTypeRemoteA2A:
return map[string]any{
"type": "a2a_preview",
"name": conn.Name,
"project_connection_id": conn.ID,
Comment thread
hund030 marked this conversation as resolved.
}, nil

case connections.ConnectionTypeGroundingWithCustomSearch:
if instanceName == "" {
return nil, exterrors.Validation(
exterrors.CodeMissingInstanceName,
fmt.Sprintf(
"connection %q is a GroundingWithCustomSearch connection; "+
"--instance-name is required",
conn.Name,
),
"pass --instance-name <name> with the Bing custom-search configuration name",
)
}
return map[string]any{
"type": "web_search",
"name": conn.Name,
Comment thread
hund030 marked this conversation as resolved.
"custom_search_configuration": map[string]any{
"project_connection_id": conn.ID,
"instance_name": instanceName,
},
}, nil

default:
return nil, exterrors.Validation(
exterrors.CodeUnsupportedConnectionCategory,
fmt.Sprintf(
"connection %q has category %q which is not supported as a toolbox tool today; "+
"v1 supports RemoteTool (MCP) and CognitiveSearch (Azure AI Search) only",
"supported categories: RemoteTool (MCP), CognitiveSearch (Azure AI Search), "+
"RemoteA2A, GroundingWithCustomSearch",
conn.Name, conn.Category,
),
"use a RemoteTool (MCP) or CognitiveSearch (Azure AI Search) connection, "+
"use one of the supported connection categories, "+
"or file an issue requesting support for the connection category you need",
)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,9 @@ import (

// connectionAddFlags carries the verb-specific flags for `connection add`.
type connectionAddFlags struct {
index string
fromFile string
index string
instanceName string
fromFile string
}

// newToolboxConnectionAddCommand returns the `connection add` command.
Expand All @@ -36,15 +37,16 @@ This command has two modes:

Single-connection mode:

azd ai toolbox add <toolbox> <connection> [--index <name>]
azd ai toolbox connection add <toolbox> <connection> [--index <name>] [--instance-name <name>]

Pass the project connection's short name as the positional. --index is
required when the connection's category is CognitiveSearch (Azure AI Search).
--instance-name is required when the category is GroundingWithCustomSearch.
Only one tool is appended; the new version becomes the default.

File mode:

azd ai toolbox add <toolbox> --from-file <path>
azd ai toolbox connection add <toolbox> --from-file <path>

Provide a JSON or YAML file with multiple connections. All inputs from a
single invocation publish exactly one new toolbox version, so adding three
Expand All @@ -57,13 +59,16 @@ At least one connection must be provided.
Examples:

# Attach a single RemoteTool (MCP) connection
azd ai toolbox add research my-mcp
azd ai toolbox connection add research my-mcp

# Attach a CognitiveSearch connection with an explicit index
azd ai toolbox add research my-search --index products
azd ai toolbox connection add research my-search --index products

# Attach a GroundingWithCustomSearch connection with a Bing custom-search instance
azd ai toolbox connection add research my-bing --instance-name docs-config

# Attach several tools in one new version
azd ai toolbox add research --from-file ./tools.yaml --output json
azd ai toolbox connection add research --from-file ./tools.yaml --output json
`,
Args: func(cmd *cobra.Command, args []string) error {
fromFile, _ := cmd.Flags().GetString("from-file")
Expand Down Expand Up @@ -93,7 +98,12 @@ Examples:

cmd.Flags().StringVar(
&flags.index, "index", "",
"Search index name. Required for CognitiveSearch (Azure AI Search) connections; ignored otherwise.",
"Search index name. Only valid for CognitiveSearch (Azure AI Search) connections; required there.",
)
cmd.Flags().StringVar(
&flags.instanceName, "instance-name", "",
"Bing custom-search configuration name. "+
"Only valid for GroundingWithCustomSearch connections; required there.",
)
cmd.Flags().StringVar(
&flags.fromFile, "from-file", "",
Expand Down Expand Up @@ -175,6 +185,13 @@ func runConnectionAddWith(
"set connection indexes in the file under connections[].index",
)
}
if verb.instanceName != "" {
return exterrors.Validation(
exterrors.CodeUnsupportedInstanceNameFlag,
"--instance-name cannot be used together with --from-file",
"set connection instance names in the file under connections[].instance_name",
)
}

var input toolboxToolsFile
if err := parseToolboxFile(verb.fromFile, &input); err != nil {
Expand All @@ -193,7 +210,7 @@ func runConnectionAddWith(
if err != nil {
return err
}
entry, err := buildToolEntry(conn, verb.index)
entry, err := buildToolEntry(conn, verb.index, verb.instanceName)
if err != nil {
return err
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -79,13 +79,17 @@ func runConnectionListWith(
// extractConnectionTools collapses the tool list to one row per connection-backed
// entry, surfacing the short connection name parsed from the trailing segment
// of the connection ARM ID (the `connection` column in `connection list`).
//
// Rows are emitted for every tool entry that references at least one
// project_connection_id. Built-in tool types (code_interpreter, file_search,
// etc.) carry no connection reference and are skipped automatically.
func extractConnectionTools(tools []map[string]any) []map[string]string {
rows := []map[string]string{}
for _, t := range tools {
toolType, _ := t["type"].(string)
toolName, _ := t["name"].(string)
switch toolType {
case "mcp":
case "mcp", "a2a_preview":
if id, ok := t["project_connection_id"].(string); ok && id != "" {
rows = append(rows, map[string]string{
"name": toolName,
Expand Down Expand Up @@ -114,6 +118,25 @@ func extractConnectionTools(tools []map[string]any) []map[string]string {
}
}
}
case "web_search":
// Built-in web_search has no custom_search_configuration; only the
// GroundingWithCustomSearch variant carries a project_connection_id.
cfg, _ := t["custom_search_configuration"].(map[string]any)
if cfg == nil {
continue
}
id, _ := cfg["project_connection_id"].(string)
if id == "" {
continue
}
instance, _ := cfg["instance_name"].(string)
rows = append(rows, map[string]string{
"name": toolName,
"connection": shortConnectionName(id),
"connection_id": id,
"type": toolType,
"instance_name": instance,
})
Comment thread
hund030 marked this conversation as resolved.
}
}
return rows
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -187,7 +187,7 @@ func resolveConnectionSpecs(
if err != nil {
return nil, err
}
entry, err := buildToolEntry(conn, spec.Index)
entry, err := buildToolEntry(conn, spec.Index, spec.InstanceName)
if err != nil {
return nil, err
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,11 @@ import (

// toolboxConnectionSpec is one connection-backed tool input.
// For CognitiveSearch connections, Index is required.
// For GroundingWithCustomSearch connections, InstanceName is required.
type toolboxConnectionSpec struct {
Name string `json:"name" yaml:"name"`
Index string `json:"index,omitempty" yaml:"index,omitempty"`
Name string `json:"name" yaml:"name"`
Index string `json:"index,omitempty" yaml:"index,omitempty"`
InstanceName string `json:"instance_name,omitempty" yaml:"instance_name,omitempty"`
}

// toolboxToolsFile is the file shape for `toolbox connection add --from-file`.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,9 @@ func fileShapeBlurb(includeDescription bool) string {
"description": "research toolbox",
"connections": [
{ "name": "my-mcp" },
{ "name": "my-search", "index": "products" }
{ "name": "my-search", "index": "products" },
{ "name": "my-bing", "instance_name": "docs-config" },
{ "name": "my-a2a" }
]
}

Expand All @@ -28,15 +30,20 @@ Equivalent YAML:
- name: my-mcp
- name: my-search
index: products
- name: my-bing
instance_name: docs-config
- name: my-a2a

Fields:
description Optional. Stored on the initial toolbox version.
connections Required. List of existing project connections to attach.
Each entry needs 'name' (the project connection short name).
'index' is required only for CognitiveSearch connections and
is the search index name inside that service.
Supported connection categories: RemoteTool (MCP),
CognitiveSearch (Azure AI Search).
description Optional. Stored on the initial toolbox version.
connections Required. List of existing project connections to attach.
Each entry needs 'name' (the project connection short name).
'index' is required only for CognitiveSearch connections.
'instance_name' is required only for
GroundingWithCustomSearch connections.
Supported connection categories: RemoteTool (MCP),
CognitiveSearch (Azure AI Search), RemoteA2A,
GroundingWithCustomSearch.

Project connections must already exist on the Foundry project; this command
does not create them. Run 'azd ai agent connection list' to see available
Expand All @@ -48,7 +55,9 @@ connections.`
{
"connections": [
{ "name": "my-mcp" },
{ "name": "my-search", "index": "products" }
{ "name": "my-search", "index": "products" },
{ "name": "my-bing", "instance_name": "docs-config" },
{ "name": "my-a2a" }
]
}

Expand All @@ -58,14 +67,19 @@ Equivalent YAML:
- name: my-mcp
- name: my-search
index: products
- name: my-bing
instance_name: docs-config
- name: my-a2a

Fields:
connections Required. List of existing project connections to attach.
Each entry needs 'name' (the project connection short name).
'index' is required only for CognitiveSearch connections and
is the search index name inside that service.
Supported connection categories: RemoteTool (MCP),
CognitiveSearch (Azure AI Search).
connections Required. List of existing project connections to attach.
Each entry needs 'name' (the project connection short name).
'index' is required only for CognitiveSearch connections.
'instance_name' is required only for
GroundingWithCustomSearch connections.
Supported connection categories: RemoteTool (MCP),
CognitiveSearch (Azure AI Search), RemoteA2A,
GroundingWithCustomSearch.

The toolbox's existing description is carried forward unchanged; use
'azd ai toolbox update' to change it.
Expand Down
Loading
Loading