Public/New-MetroAIResource.ps1

function New-MetroAIResource {
    <#
    .SYNOPSIS
        Creates a new Metro AI agent or assistant resource with comprehensive tool support.
    .DESCRIPTION
        Creates an Azure AI agent or assistant with various tools including connected agents, code interpreter,
        file search, Azure AI Search, custom functions, OpenAPI integrations, and MCP (Model Context Protocol) servers.
        Follows Azure best practices for parameter validation, error handling, and resource management.
        Note: Bing grounding must be added after creation using Set-MetroAIResource.
        Can create from JSON file, pipeline input object (for copying), or specify individual parameters.
    .PARAMETER InputObject
        Pipeline input object from Get-MetroAIResource. When used, creates a copy of the existing resource with a new name.
    .PARAMETER InputFile
        Path to a JSON file containing the complete resource definition to create.
    .PARAMETER Model
        The model identifier (e.g., 'gpt-4', 'gpt-4-turbo', 'gpt-35-turbo').
    .PARAMETER Name
        The name of the agent/assistant resource. Must be unique within the workspace.
    .PARAMETER Description
        Optional description for the resource (max 512 characters).
    .PARAMETER Instructions
        System instructions that guide the agent's behavior (max 256,000 characters).
    .PARAMETER Metadata
        Optional metadata as key-value pairs (max 16 pairs, keys/values max 64 chars each).
    .PARAMETER ResponseFormat
        Response format specification. Use 'text' or structured format definitions.
    .PARAMETER Temperature
        Sampling temperature between 0.0 (deterministic) and 2.0 (most random). Default is 1.0.
    .PARAMETER TopP
        Nucleus sampling parameter between 0.0 and 1.0. Alternative to temperature.
    .PARAMETER EnableConnectedAgent
        Enable a single connected agent tool.
    .PARAMETER ConnectedAgentId
        ID of the connected agent. Required when EnableConnectedAgent is used.
    .PARAMETER ConnectedAgentName
        Display name for the connected agent. Required when EnableConnectedAgent is used.
    .PARAMETER ConnectedAgentDescription
        Description of the connected agent's capabilities. Required when EnableConnectedAgent is used.
    .PARAMETER ConnectedAgentsDefinition
        Array of connected agent definitions. Each must have 'id', 'name', and 'description' properties.
    .PARAMETER EnableCodeInterpreter
        Enable Python code execution capabilities.
    .PARAMETER CodeInterpreterFileIds
        Array of file IDs to make available to the code interpreter.
    .PARAMETER EnableFileSearch
        Enable file search capabilities across vector stores.
    .PARAMETER FileSearchVectorStoreIds
        Array of vector store IDs for file search operations.
    .PARAMETER EnableAzureAiSearch
        Enable Azure AI Search integration.
    .PARAMETER AzureAiSearchIndexes
        Array of Azure AI Search index configurations.
    .PARAMETER EnableFunctionTool
        Enable custom function calling.
    .PARAMETER FunctionName
        Name of the custom function.
    .PARAMETER FunctionDescription
        Description of what the function does.
    .PARAMETER FunctionParameters
        JSON Schema defining the function's parameters.
    .PARAMETER EnableOpenApi
        Enable OpenAPI/REST API integration.
    .PARAMETER OpenApiDefinitionFile
        Path to the OpenAPI specification file (JSON format only).
    .PARAMETER OpenApiName
        Friendly name for the OpenAPI integration.
    .PARAMETER OpenApiDescription
        Description of the OpenAPI service.
    .PARAMETER OpenApiAuthType
        Authentication method: 'Anonymous', 'Connection', or 'ManagedIdentity'.
    .PARAMETER OpenApiConnectionId
        Connection ID for API authentication when using 'Connection' auth type.
    .PARAMETER OpenApiManagedAudience
        Target audience URI when using 'ManagedIdentity' auth type.
    .PARAMETER EnableMcp
        Enable Model Context Protocol (MCP) server integration.
    .PARAMETER McpServerLabel
        Unique label/name for the MCP server (max 256 characters).
    .PARAMETER McpServerUrl
        URL of the MCP server endpoint. Must start with http:// or https://.
    .PARAMETER McpRequireApproval
        Approval policy for MCP server actions: 'never', 'once', or 'always'. Default is 'never'.
    .PARAMETER McpServersConfiguration
        Array of MCP server configurations. Each must have 'server_label', 'server_url', and optionally 'require_approval' and 'allowed_tools' properties.
        The 'allowed_tools' property should be an array of strings specifying which tools the agent can use from that MCP server.
    .EXAMPLE
        New-MetroAIResource -Model 'gpt-4.1' -Name 'MyAssistant' -Description 'General purpose assistant'
    .EXAMPLE
        New-MetroAIResource -InputFile './existing-assistant.json'
    .EXAMPLE
        # Copy an existing agent with a new name
        $Agent = Get-MetroAIAgent -AssistantId 'asst-123'
        $Agent | New-MetroAIAgent -Name 'CopiedAgent'
    .EXAMPLE
        New-MetroAIResource -Model 'gpt-4.1' -Name 'CodeHelper' -EnableCodeInterpreter -CodeInterpreterFileIds @('file-123')
    .EXAMPLE
        # Create assistant with MCP server integration
        New-MetroAIResource -Model 'gpt-4.1' -Name 'MCPBot' -EnableMcp -McpServerLabel 'DatabaseServer' -McpServerUrl 'https://api.example.com/mcp' -McpRequireApproval 'once'
    .EXAMPLE
        # Create assistant with multiple MCP servers
        $mcpServers = @(
            @{ server_label = 'WeatherAPI'; server_url = 'https://weather.example.com/mcp'; require_approval = 'never' },
            @{ server_label = 'DatabaseAPI'; server_url = 'https://db.example.com/mcp'; require_approval = 'once'; allowed_tools = @('query_db', 'update_record') }
        )
        New-MetroAIResource -Model 'gpt-4.1' -Name 'MultiMCPBot' -McpServersConfiguration $mcpServers
    .EXAMPLE
        # Create assistant, then add Bing grounding
        $assistant = New-MetroAIResource -Model 'gpt-4.1' -Name 'SearchBot'
        Set-MetroAIResource -AssistantId $assistant.id -EnableBingGrounding -BingConnectionId 'bing-search-connection'
    .EXAMPLE
        # Multi-tool agent with various capabilities including MCP
        New-MetroAIResource -Model 'gpt-4.1' -Name 'MultiToolAgent' `
            -EnableCodeInterpreter -CodeInterpreterFileIds @('file-123') `
            -EnableAzureAiSearch -AzureAiSearchIndexes @(@{ index_connection_id='search-conn'; index_name='docs'; query_type='semantic'; top_k=5 }) `
            -EnableMcp -McpServerLabel 'CustomAPI' -McpServerUrl 'https://api.example.com/mcp'
    .NOTES
        Requires Set-MetroAIContext to be called first. Follow Azure AI responsible AI guidelines.
        To add Bing grounding, use Set-MetroAIResource with -EnableBingGrounding after creation.
        When using InputObject from pipeline, system-generated properties are automatically excluded.
    #>

    [Alias("New-MetroAIAgent")]
    [Alias("New-MetroAIAssistant")]
    [CmdletBinding(DefaultParameterSetName = 'Parameters', SupportsShouldProcess, ConfirmImpact = 'Low')]
    param (
        [Parameter(Mandatory = $true, ParameterSetName = 'InputObject', ValueFromPipeline = $true)]
        [ValidateNotNull()]
        [object]$InputObject,

        [Parameter(Mandatory = $true, ParameterSetName = 'Json')]
        [ValidateScript({
                if (-not (Test-Path $_ -PathType Leaf)) { throw "Input file not found: $_" }
                $extension = [System.IO.Path]::GetExtension($_).ToLower()
                if ($extension -ne '.json') { throw "Input file must be JSON format (.json)" }
                return $true
            })]
        [string]$InputFile,

        [Parameter(Mandatory, ParameterSetName = 'Parameters')]
        [Parameter(ParameterSetName = 'InputObject')]
        [ValidateNotNullOrEmpty()]
        [ValidateLength(1, 256)]
        [string]$Model,

        [Parameter(Mandatory, ParameterSetName = 'Parameters')]
        [Parameter(ParameterSetName = 'InputObject')]
        [ValidateNotNullOrEmpty()]
        [ValidateLength(1, 256)]
        [Alias('ResourceName')]
        [string]$Name,

        [Parameter(ParameterSetName = 'Parameters')]
        [Parameter(ParameterSetName = 'InputObject')]
        [ValidateLength(0, 512)]
        [string]$Description,

        [Parameter(ParameterSetName = 'Parameters')]
        [Parameter(ParameterSetName = 'InputObject')]
        [ValidateLength(0, 256000)]
        [string]$Instructions,

        [Parameter(ParameterSetName = 'Parameters')]
        [Parameter(ParameterSetName = 'InputObject')]
        [ValidateScript({
                if ($_.Count -gt 16) { throw "Maximum 16 metadata entries allowed" }
                foreach ($key in $_.Keys) {
                    if ($key.Length -gt 64) { throw "Metadata key '$key' exceeds 64 character limit" }
                    if ($_[$key].ToString().Length -gt 512) { throw "Metadata value for '$key' exceeds 512 character limit" }
                }
                return $true
            })]
        [hashtable]$Metadata,

        [Parameter(ParameterSetName = 'Parameters')]
        [Parameter(ParameterSetName = 'InputObject')]
        [string]$ResponseFormat,

        [Parameter(ParameterSetName = 'Parameters')]
        [Parameter(ParameterSetName = 'InputObject')]
        [ValidateRange(0.0, 2.0)]
        [double]$Temperature,

        [Parameter(ParameterSetName = 'Parameters')]
        [Parameter(ParameterSetName = 'InputObject')]
        [ValidateRange(0.0, 1.0)]
        [double]$TopP,

        # Connected Agent Parameters
        [Parameter(ParameterSetName = 'Parameters')]
        [switch]$EnableConnectedAgent,

        [Parameter(ParameterSetName = 'Parameters')]
        [ValidateNotNullOrEmpty()]
        [string]$ConnectedAgentId,

        [Parameter(ParameterSetName = 'Parameters')]
        [ValidateLength(1, 256)]
        [string]$ConnectedAgentName,

        [Parameter(ParameterSetName = 'Parameters')]
        [ValidateLength(1, 512)]
        [string]$ConnectedAgentDescription,

        [Parameter(ParameterSetName = 'Parameters')]
        [ValidateScript({
                foreach ($agent in $_) {
                    if (-not ($agent.id -and $agent.name -and $agent.description)) {
                        throw "Each ConnectedAgentsDefinition entry must include 'id', 'name', and 'description' properties"
                    }
                    if ($agent.name.Length -gt 256) { throw "Connected agent name exceeds 256 characters" }
                    if ($agent.description.Length -gt 512) { throw "Connected agent description exceeds 512 characters" }
                }
                return $true
            })]
        [object[]]$ConnectedAgentsDefinition,

        # Code Interpreter Parameters
        [Parameter(ParameterSetName = 'Parameters')]
        [switch]$EnableCodeInterpreter,

        [Parameter(ParameterSetName = 'Parameters')]
        [ValidateCount(0, 20)]
        [string[]]$CodeInterpreterFileIds,

        # File Search Parameters
        [Parameter(ParameterSetName = 'Parameters')]
        [switch]$EnableFileSearch,

        [Parameter(ParameterSetName = 'Parameters')]
        [ValidateCount(1, 1)]
        [string[]]$FileSearchVectorStoreIds,

        # Azure AI Search Parameters
        [Parameter(ParameterSetName = 'Parameters')]
        [switch]$EnableAzureAiSearch,

        [Parameter(ParameterSetName = 'Parameters')]
        [ValidateScript({
                foreach ($index in $_) {
                    $requiredProps = @('index_connection_id', 'index_name')
                    foreach ($prop in $requiredProps) {
                        if (-not $index.$prop) { throw "Azure AI Search index missing required property: $prop" }
                    }
                }
                return $true
            })]
        [object[]]$AzureAiSearchIndexes,

        # Function Tool Parameters
        [Parameter(ParameterSetName = 'Parameters')]
        [switch]$EnableFunctionTool,

        [Parameter(ParameterSetName = 'Parameters')]
        [ValidatePattern('^[a-zA-Z0-9_-]+$')]
        [ValidateLength(1, 64)]
        [string]$FunctionName,

        [Parameter(ParameterSetName = 'Parameters')]
        [ValidateLength(1, 1024)]
        [string]$FunctionDescription,

        [Parameter(ParameterSetName = 'Parameters')]
        [ValidateScript({
                if ($_.type -ne 'object') { throw "Function parameters must have type 'object'" }
                if (-not $_.properties) { throw "Function parameters must include 'properties'" }
                return $true
            })]
        [hashtable]$FunctionParameters,

        # OpenAPI Parameters
        [Parameter(ParameterSetName = 'Parameters')]
        [switch]$EnableOpenApi,

        [Parameter(ParameterSetName = 'Parameters')]
        [ValidateScript({
                if (-not (Test-Path $_ -PathType Leaf)) { throw "OpenAPI definition file not found: $_" }
                $extension = [System.IO.Path]::GetExtension($_).ToLower()
                if ($extension -ne '.json') {
                    throw "OpenAPI file must be JSON format (.json). YAML is not supported."
                }
                return $true
            })]
        [string]$OpenApiDefinitionFile,

        [Parameter(ParameterSetName = 'Parameters')]
        [ValidateLength(1, 64)]
        [string]$OpenApiName,

        [Parameter(ParameterSetName = 'Parameters')]
        [ValidateLength(1, 1024)]
        [string]$OpenApiDescription,

        [Parameter(ParameterSetName = 'Parameters')]
        [ValidateSet('Anonymous', 'Connection', 'ManagedIdentity')]
        [string]$OpenApiAuthType,

        [Parameter(ParameterSetName = 'Parameters')]
        [ValidateNotNullOrEmpty()]
        [string]$OpenApiConnectionId,

        [Parameter(ParameterSetName = 'Parameters')]
        [ValidatePattern('^https?://')]
        [string]$OpenApiManagedAudience,

        # MCP Server Parameters
        [Parameter(ParameterSetName = 'Parameters')]
        [switch]$EnableMcp,

        [Parameter(ParameterSetName = 'Parameters')]
        [ValidateLength(1, 256)]
        [string]$McpServerLabel,

        [Parameter(ParameterSetName = 'Parameters')]
        [ValidatePattern('^https?://')]
        [ValidateNotNullOrEmpty()]
        [string]$McpServerUrl,

        [Parameter(ParameterSetName = 'Parameters')]
        [ValidateSet('never', 'once', 'always')]
        [string]$McpRequireApproval = 'never',

        [Parameter(ParameterSetName = 'Parameters')]
        [ValidateScript({
                foreach ($server in $_) {
                    if (-not ($server.server_label -and $server.server_url)) {
                        throw "Each McpServersConfiguration entry must include 'server_label' and 'server_url' properties"
                    }
                    if ($server.server_label.Length -gt 256) { throw "MCP server label exceeds 256 characters" }
                    if ($server.server_url -notmatch '^https?://') { throw "MCP server URL must start with http:// or https://" }
                    if ($server.require_approval -and $server.require_approval -notin @('never', 'once', 'always')) {
                        throw "MCP server require_approval must be 'never', 'once', or 'always'"
                    }
                    if ($server.allowed_tools -and $server.allowed_tools -isnot [array]) {
                        throw "MCP server allowed_tools must be an array of strings"
                    }
                }
                return $true
            })]
        [object[]]$McpServersConfiguration
    )

    begin {
        Write-Verbose "Starting New-MetroAIResource"

        # Ensure context is set
        if (-not $script:MetroContext) {
            throw "Metro AI context not set. Use Set-MetroAIContext first."
        }

        Write-Verbose "Using $($script:MetroContext.ApiType) API at $($script:MetroContext.Endpoint)"
    }

    process {
        try {
            # Handle JSON file input
            if ($PSCmdlet.ParameterSetName -eq 'Json') {
                Write-Verbose "Processing input file: $InputFile"

                try {
                    $body = Get-Content -Path $InputFile -Raw -ErrorAction Stop | ConvertFrom-Json -Depth 100 -NoEnumerate -ErrorAction Stop
                    Write-Verbose "Successfully parsed JSON input file"
                }
                catch {
                    throw "Failed to parse JSON input file '$InputFile': $($_.Exception.Message)"
                }

                # Clean up auto-generated properties
                $body = Remove-MetroAIAutoGeneratedProperties -InputObject $body

                # Extract name from JSON for confirmation message
                $resourceName = if ($body.name) { $body.name } else { "Unnamed Resource" }
                $resourceModel = if ($body.model) { $body.model } else { "Unknown Model" }

                $confirmMessage = "Create new $($script:MetroContext.ApiType) '$resourceName' with model '$resourceModel' from file '$InputFile'"
                if ($body.tools -and $body.tools.Count -gt 0) {
                    $toolsList = ($body.tools | ForEach-Object { $_.type }) -join ', '
                    $confirmMessage += " and tools: $toolsList"
                }
            }
            elseif ($PSCmdlet.ParameterSetName -eq 'InputObject') {
                # Handle pipeline input for copying existing resources
                Write-Verbose "Processing pipeline input object for copying"

                if (-not $InputObject.id) {
                    throw "Input object must have an 'id' property"
                }

                Write-Verbose "Copying resource from ID: $($InputObject.id)"

                # Clean up auto-generated properties and convert to manageable object
                $body = Remove-MetroAIAutoGeneratedProperties -InputObject $InputObject | ConvertTo-Json -Depth 100 | ConvertFrom-Json

                # Override with any explicitly provided parameters (Name and Model are required for InputObject parameter set)
                if ($PSBoundParameters.ContainsKey('Model')) {
                    $body.model = $Model
                    Write-Verbose "Overriding model with: $Model"
                }
                if ($PSBoundParameters.ContainsKey('Name')) {
                    $body.name = $Name
                    Write-Verbose "Setting new name: $Name"
                }
                else {
                    throw "Name parameter is required when copying from InputObject"
                }

                # Override other optional parameters if provided
                if ($PSBoundParameters.ContainsKey('Description')) { $body.description = $Description }
                if ($PSBoundParameters.ContainsKey('Instructions')) { $body.instructions = $Instructions }
                if ($PSBoundParameters.ContainsKey('Metadata')) { $body.metadata = $Metadata }
                if ($PSBoundParameters.ContainsKey('ResponseFormat')) { $body.response_format = $ResponseFormat }
                if ($PSBoundParameters.ContainsKey('Temperature')) { $body.temperature = $Temperature }
                if ($PSBoundParameters.ContainsKey('TopP')) { $body.top_p = $TopP }

                $confirmMessage = "Create new $($script:MetroContext.ApiType) '$Name' as copy of '$($InputObject.name)' (ID: $($InputObject.id))"
                if ($body.tools -and $body.tools.Count -gt 0) {
                    $toolsList = ($body.tools | ForEach-Object { $_.type }) -join ', '
                    $confirmMessage += " with tools: $toolsList"
                }
            }
            else {
                # Handle parameter-based creation (existing logic)
                Write-Verbose "Processing parameter-based creation with Model: $Model, Name: $Name"

                # Enhanced parameter validation with detailed error messages
                if ($EnableConnectedAgent -and $ConnectedAgentsDefinition) {
                    throw "Cannot use both -EnableConnectedAgent and -ConnectedAgentsDefinition simultaneously. Choose one approach."
                }

                if ($EnableMcp -and $McpServersConfiguration) {
                    throw "Cannot use both -EnableMcp and -McpServersConfiguration simultaneously. Choose one approach."
                }

                if ($EnableConnectedAgent) {
                    $requiredParams = @('ConnectedAgentId', 'ConnectedAgentName', 'ConnectedAgentDescription')
                    foreach ($param in $requiredParams) {
                        if (-not $PSBoundParameters[$param]) {
                            throw "Parameter '$param' is required when using -EnableConnectedAgent."
                        }
                    }

                    # Validate connected agent exists with detailed error handling
                    Write-Verbose "Validating connected agent: $ConnectedAgentId"
                    try {
                        $connectedAgent = Get-MetroAIResource -AssistantId $ConnectedAgentId -ErrorAction Stop
                        Write-Verbose "Connected agent '$($connectedAgent.name)' validated successfully"
                    }
                    catch {
                        throw "Connected agent '$ConnectedAgentId' not found or inaccessible. Verify the ID exists and you have access: $($_.Exception.Message)"
                    }
                }

                if ($ConnectedAgentsDefinition) {
                    Write-Verbose "Validating $($ConnectedAgentsDefinition.Count) connected agents"
                    foreach ($agent in $ConnectedAgentsDefinition) {
                        try {
                            $connectedAgent = Get-MetroAIResource -AssistantId $agent.id -ErrorAction Stop
                            Write-Verbose "Connected agent '$($agent.name)' (ID: $($agent.id)) validated successfully"
                        }
                        catch {
                            throw "Connected agent '$($agent.id)' not found or inaccessible. Verify the ID exists and you have access: $($_.Exception.Message)"
                        }
                    }
                }

                # Tool-specific validation with helpful error messages
                if ($EnableCodeInterpreter -and -not $PSBoundParameters.ContainsKey('CodeInterpreterFileIds')) {
                    throw "CodeInterpreterFileIds parameter is required when EnableCodeInterpreter is specified. Provide an array of file IDs or use an empty array @() if no files are needed initially."
                }

                if ($EnableFileSearch -and -not $FileSearchVectorStoreIds) {
                    throw "FileSearchVectorStoreIds parameter is required when EnableFileSearch is specified."
                }

                if ($EnableAzureAiSearch -and -not $AzureAiSearchIndexes) {
                    throw "AzureAiSearchIndexes parameter is required when EnableAzureAiSearch is specified."
                }

                if ($EnableFileSearch -and -not $FileSearchVectorStoreIds) {
                    throw "FileSearchVectorStoreIds parameter is required when EnableFileSearch is specified."
                }

                if ($EnableFunctionTool) {
                    $requiredFuncParams = @('FunctionName', 'FunctionDescription', 'FunctionParameters')
                    foreach ($param in $requiredFuncParams) {
                        if (-not $PSBoundParameters[$param]) {
                            throw "Parameter '$param' is required when using -EnableFunctionTool"
                        }
                    }
                }

                if ($EnableOpenApi) {
                    $requiredApiParams = @('OpenApiDefinitionFile', 'OpenApiName', 'OpenApiDescription', 'OpenApiAuthType')
                    foreach ($param in $requiredApiParams) {
                        if (-not $PSBoundParameters[$param]) {
                            throw "Parameter '$param' is required when using -EnableOpenApi"
                        }
                    }

                    if ($OpenApiAuthType -eq 'Connection' -and -not $OpenApiConnectionId) {
                        throw "OpenApiConnectionId is required when OpenApiAuthType is 'Connection'"
                    }

                    if ($OpenApiAuthType -eq 'ManagedIdentity' -and -not $OpenApiManagedAudience) {
                        throw "OpenApiManagedAudience is required when OpenApiAuthType is 'ManagedIdentity'"
                    }
                }

                if ($EnableMcp) {
                    $requiredMcpParams = @('McpServerLabel', 'McpServerUrl')
                    foreach ($param in $requiredMcpParams) {
                        if (-not $PSBoundParameters[$param]) {
                            throw "Parameter '$param' is required when using -EnableMcp"
                        }
                    }
                }

                if ($McpServersConfiguration) {
                    Write-Verbose "Validating $($McpServersConfiguration.Count) MCP server configurations"
                    foreach ($server in $McpServersConfiguration) {
                        if (-not ($server.server_label -and $server.server_url)) {
                            throw "Each MCP server configuration must include 'server_label' and 'server_url' properties"
                        }
                        Write-Verbose "MCP server configuration validated: $($server.server_label) at $($server.server_url)"
                    }
                }

                if ($EnableBingGrounding -and -not $BingConnectionId) {
                    throw "BingConnectionId is required when EnableBingGrounding is specified. Only connection-based authentication is supported for Bing grounding."
                }

                # Temperature and TopP mutual exclusivity check
                if ($PSBoundParameters.ContainsKey('Temperature') -and $PSBoundParameters.ContainsKey('TopP')) {
                    Write-Warning "Both Temperature and TopP specified. The API may use only one. Consider using only Temperature or only TopP for predictable behavior."
                }

                # Build request body with proper null checking
                $body = @{
                    model = $Model
                    name  = $Name
                }

                # Add optional core properties only if they have values
                if ($Description) { $body.description = $Description }
                if ($Instructions) { $body.instructions = $Instructions }
                if ($Metadata -and $Metadata.Count -gt 0) { $body.metadata = $Metadata }
                if ($ResponseFormat) { $body.response_format = $ResponseFormat }
                if ($PSBoundParameters.ContainsKey('Temperature')) { $body.temperature = $Temperature }
                if ($PSBoundParameters.ContainsKey('TopP')) { $body.top_p = $TopP }

                # Assemble tools and resources using Lists for better performance
                $tools = [System.Collections.Generic.List[hashtable]]::new()
                $toolResources = @{}

                # Connected Agent Tools
                if ($EnableConnectedAgent) {
                    $tools.Add(@{
                            type            = 'connected_agent'
                            connected_agent = @{
                                id          = $ConnectedAgentId
                                name        = $ConnectedAgentName
                                description = $ConnectedAgentDescription
                            }
                        })
                    Write-Verbose "Added connected agent tool: $ConnectedAgentName"
                }

                if ($ConnectedAgentsDefinition) {
                    foreach ($agent in $ConnectedAgentsDefinition) {
                        $tools.Add(@{
                                type            = 'connected_agent'
                                connected_agent = @{
                                    id          = $agent.id
                                    name        = $agent.name
                                    description = $agent.description
                                }
                            })
                        Write-Verbose "Added connected agent tool: $($agent.name)"
                    }
                }

                # Code Interpreter Tool
                if ($EnableCodeInterpreter) {
                    $tools.Add(@{ type = 'code_interpreter' })
                    $fileIds = if ($CodeInterpreterFileIds -and $CodeInterpreterFileIds.Count -gt 0) {
                        # Force array conversion using the comma operator to handle single items
                        , $CodeInterpreterFileIds
                    }
                    else {
                        @()
                    }
                    $toolResources.code_interpreter = @{ file_ids = $fileIds }
                    Write-Verbose "Added code interpreter tool with $($fileIds.Count) files"
                }

                # File Search Tool
                if ($EnableFileSearch) {
                    $tools.Add(@{ type = 'file_search' })
                    $toolResources.file_search = @{ vector_store_ids = $FileSearchVectorStoreIds }
                    Write-Verbose "Added file search tool with $($FileSearchVectorStoreIds.Count) vector stores"
                }

                # Azure AI Search Tool
                if ($EnableAzureAiSearch) {
                    $tools.Add(@{ type = 'azure_ai_search' })
                    $toolResources.azure_ai_search = @{ indexes = $AzureAiSearchIndexes }
                    Write-Verbose "Added Azure AI Search tool with $($AzureAiSearchIndexes.Count) indexes"
                }

                # Function Tool
                if ($EnableFunctionTool) {
                    $tools.Add(@{
                            type     = 'function'
                            function = @{
                                name        = $FunctionName
                                description = $FunctionDescription
                                parameters  = $FunctionParameters
                            }
                        })
                    Write-Verbose "Added function tool: $FunctionName"
                }

                # OpenAPI Tool
                if ($EnableOpenApi) {
                    Write-Verbose "Loading OpenAPI specification from: $OpenApiDefinitionFile"

                    try {
                        $specContent = Get-Content -Path $OpenApiDefinitionFile -Raw -ErrorAction Stop
                        $spec = $specContent | ConvertFrom-Json -ErrorAction Stop

                        $auth = switch ($OpenApiAuthType) {
                            'Anonymous' { @{ type = 'anonymous' } }
                            'Connection' { @{ type = 'connection'; connection_id = $OpenApiConnectionId } }
                            'ManagedIdentity' { @{ type = 'managed_identity'; security_scheme = @{ audience = $OpenApiManagedAudience } } }
                        }

                        $tools.Add(@{
                                type    = 'openapi'
                                openapi = @{
                                    name        = $OpenApiName
                                    description = $OpenApiDescription
                                    spec        = $spec
                                    auth        = $auth
                                }
                            })
                        Write-Verbose "Added OpenAPI tool: $OpenApiName ($OpenApiAuthType auth)"
                    }
                    catch {
                        throw "Failed to process OpenAPI definition file '$OpenApiDefinitionFile': $($_.Exception.Message)"
                    }
                }

                # MCP Server Tool
                if ($EnableMcp) {
                    Write-Verbose "Configuring MCP server tool: $McpServerLabel at $McpServerUrl"

                    $tools.Add(@{
                            type             = 'mcp'
                            server_label     = $McpServerLabel
                            server_url       = $McpServerUrl
                            require_approval = $McpRequireApproval
                        })
                    Write-Verbose "Added MCP server tool: $McpServerLabel with approval policy: $McpRequireApproval"
                }

                if ($McpServersConfiguration) {
                    Write-Verbose "Adding $($McpServersConfiguration.Count) MCP server configurations"
                    foreach ($server in $McpServersConfiguration) {
                        $mcpTool = @{
                            type             = 'mcp'
                            server_label     = $server.server_label
                            server_url       = $server.server_url
                            require_approval = if ($server.require_approval) { $server.require_approval } else { 'never' }
                        }
                        
                        # Add allowed_tools if specified
                        if ($server.allowed_tools) {
                            $mcpTool.allowed_tools = $server.allowed_tools
                            Write-Verbose "Added allowed_tools for MCP server $($server.server_label): $($server.allowed_tools -join ', ')"
                        }
                        
                        $tools.Add($mcpTool)
                        Write-Verbose "Added MCP server tool: $($server.server_label) at $($server.server_url)"
                    }
                }

                # Bing Grounding Tool - Connection-based only with correct payload structure
                if ($EnableBingGrounding) {
                    Write-Verbose "Configuring Bing grounding tool with connection: $BingConnectionId"

                    $tools.Add(@{
                            type           = 'bing_grounding'
                            bing_grounding = @{
                                connections = @(
                                    @{
                                        connection_id = $BingConnectionId
                                    }
                                )
                            }
                        })
                    Write-Verbose "Added Bing grounding tool with connection ID: $BingConnectionId"
                }

                # Add tools and resources to body if any exist
                if ($tools.Count -gt 0) {
                    $body.tools = $tools.ToArray()
                    Write-Verbose "Total tools configured: $($tools.Count)"
                }

                if ($toolResources.Count -gt 0) {
                    $body.tool_resources = $toolResources
                    Write-Verbose "Tool resources configured for: $($toolResources.Keys -join ', ')"
                }

                # Create resource with confirmation
                $resourceType = $script:MetroContext.ApiType
                $confirmMessage = "Create new $resourceType '$Name' with model '$Model'"
                if ($tools.Count -gt 0) {
                    $toolsList = ($tools | ForEach-Object { $_.type }) -join ', '
                    $confirmMessage += " and tools: $toolsList"
                }
            }

            # Execute the creation with confirmation
            if ($PSCmdlet.ShouldProcess($confirmMessage, "New-MetroAIResource")) {
                Write-Verbose "Creating resource..."
                Write-Verbose "Request payload: $($body | ConvertTo-Json -Depth 100 -Compress)"

                $invokeParams = @{
                    Service     = 'assistants'
                    Operation   = 'create'
                    Method      = 'Post'
                    ContentType = 'application/json'
                    Body        = $body
                }

                $result = Invoke-MetroAIApiCall @invokeParams

                if ($result -and $result.id) {
                    $resultName = if ($body.name) { $body.name } else { $result.id }
                    Write-Information "Successfully created $($script:MetroContext.ApiType) '$resultName' with ID: $($result.id)" -InformationAction Continue

                    # Add some useful output for the user
                    if ($result.tools -and $result.tools.Count -gt 0) {
                        Write-Information "Configured tools: $($result.tools | ForEach-Object { $_.type })" -InformationAction Continue
                    }

                    Write-Verbose "Resource creation completed successfully"
                    return $result
                }
                else {
                    throw "Resource creation appeared to succeed but no ID was returned in the response. This may indicate a service issue."
                }
            }
        }
        catch {
            $errorName = if ($PSCmdlet.ParameterSetName -eq 'Json') {
                if ($body.name) { $body.name } else { "Resource from $InputFile" }
            }
            elseif ($PSCmdlet.ParameterSetName -eq 'InputObject') {
                $Name
            }
            else {
                $Name
            }
            $errorMessage = "Failed to create Metro AI resource '$errorName': $($_.Exception.Message)"
            Write-Error $errorMessage -ErrorAction Stop
        }
    }
}