"""Arcade Starter Tools for Asana

DO NOT EDIT THIS MODULE DIRECTLY.

THIS MODULE WAS AUTO-GENERATED BY TRANSPILING THE API STARTER TOOL JSON DEFINITIONS
IN THE ../wrapper_tools DIRECTORY INTO PYTHON CODE. ANY CHANGES TO THIS MODULE WILL
BE OVERWRITTEN BY THE TRANSPILER.
"""

import asyncio
import json
import jsonschema
from enum import Enum
from typing import Annotated, Any

import httpx

from arcade_tdk import tool, ToolContext
from arcade_tdk.errors import RetryableToolError
from arcade_tdk.auth import OAuth2

from .request_body_schemas import REQUEST_BODY_SCHEMAS

# Retry configuration
INITIAL_RETRY_DELAY = 0.5  # seconds

HTTP_CLIENT = httpx.AsyncClient(
    timeout=httpx.Timeout(60.0, connect=10.0),
    limits=httpx.Limits(max_keepalive_connections=20, max_connections=100),
    transport=httpx.AsyncHTTPTransport(retries=3),
    http2=True,
    follow_redirects=True,
)


class ToolMode(str, Enum):
    """Mode for tools with complex request bodies."""

    GET_REQUEST_SCHEMA = "get_request_schema"
    EXECUTE = "execute"


def remove_none_values(data: dict[str, Any]) -> dict[str, Any]:
    return {k: v for k, v in data.items() if v is not None}


async def make_request(
    url: str,
    method: str,
    params: dict[str, Any] | None = None,
    headers: dict[str, Any] | None = None,
    content: str | None = None,
    data: dict[str, Any] | None = None,
    auth: tuple[str, str] | None = None,
    max_retries: int = 3,
) -> httpx.Response:
    """Make an HTTP request with retry logic for 5xx server errors."""
    for attempt in range(max_retries):
        try:
            response = await HTTP_CLIENT.request(
                url=url,
                auth=auth,
                method=method,
                params=params,
                headers=headers,
                content=content,
            )
            response.raise_for_status()
        except httpx.HTTPStatusError as e:
            # Only retry on 5xx server errors
            if e.response.status_code >= 500 and attempt < max_retries - 1:
                # Exponential backoff: 0.5s, 1s, 2s
                await asyncio.sleep(INITIAL_RETRY_DELAY * (2**attempt))
                continue
            # Re-raise for 4xx errors or if max retries reached
            raise
        except httpx.RequestError as e:
            # Don't retry request errors (network issues are handled by transport)
            raise
        else:
            return response

    # This should never be reached, but satisfies type checker
    raise httpx.RequestError("Max retries exceeded")  # noqa: TRY003


async def make_request_with_schema_validation(
    url: str,
    method: str,
    request_data: dict[str, Any],
    schema: dict[str, Any] | str,
    auth: tuple[str, str] | None = None,
    params: dict[str, Any] | None = None,
    headers: dict[str, Any] | None = None,
    max_retries: int = 3,
) -> httpx.Response:
    """Make an HTTP request with schema validation on format errors."""
    # Parse schema if it's a string, skip validation if parsing fails
    parsed_schema = None
    if isinstance(schema, str):
        try:
            parsed_schema = json.loads(schema)
        except Exception:
            # If schema parsing fails, just skip validation
            parsed_schema = None
    else:
        parsed_schema = schema

    try:
        response = await make_request(
            url=url,
            auth=auth,
            method=method,
            params=params,
            headers=headers,
            content=json.dumps(request_data),
            max_retries=max_retries,
        )
    except httpx.HTTPStatusError as e:
        # Only provide schema validation for format-related errors
        if e.response.status_code in (400, 422):
            api_error_details = f"API returned {e.response.status_code}: {e.response.text}"

            # Only run validation if we have a valid parsed schema
            if parsed_schema is not None:
                # Run validation to provide additional context
                is_valid, validation_error = validate_json_against_schema(
                    request_data, parsed_schema
                )

                if not is_valid:
                    # Schema validation found issues - additional context
                    additional_context = (
                        f"{api_error_details}\n\n"
                        f"Schema validation found the following issues:\n"
                        f"{validation_error}"
                    )
                else:
                    # Schema validation passed - just show API error
                    additional_context = api_error_details
            else:
                # No valid schema - just show API error
                additional_context = api_error_details

            raise RetryableToolError(
                message=(f"API request failed with validation error: {e.response.status_code}"),
                developer_message=api_error_details,
                additional_prompt_content=additional_context,
            ) from e
        else:
            # For non-validation errors, re-raise as-is
            raise
    else:
        return response


def validate_json_against_schema(
    json_data: dict[str, Any], schema: dict[str, Any]
) -> tuple[bool, str | None]:
    """Validate JSON data against an OpenAPI/JSON Schema.

    This provides full JSON Schema Draft 7 validation including:
    - Required fields, types, enums
    - Pattern validation (regex)
    - Format validation (email, uuid, date-time, etc.)
    - Min/max length and values
    - oneOf, anyOf, allOf
    - And all other JSON Schema features

    Args:
        json_data: The JSON data to validate
        schema: The JSON Schema to validate against

    Returns:
        Tuple of (is_valid, error_messages). If valid, error_messages is None.
        If invalid, error_messages contains all validation errors.
    """
    try:
        validator = jsonschema.Draft7Validator(
            schema, format_checker=jsonschema.Draft7Validator.FORMAT_CHECKER
        )
        # Collect ALL validation errors
        errors = list(validator.iter_errors(json_data))
        if errors:
            # Format all errors with their paths
            error_messages = []
            for error in errors:
                error_path = ".".join(str(p) for p in error.path) if error.path else "root"
                error_messages.append(f"{error.message} at {error_path}")
            # Join all errors with newlines
            return False, "\n".join(error_messages)
        else:
            return True, None
    except jsonschema.SchemaError as e:
        return False, f"Invalid schema: {e.message}"
    except Exception as e:
        return False, f"Validation error: {e!s}"


@tool(requires_auth=OAuth2(id="arcade-asana"))
async def get_pending_access_requests(
    context: ToolContext,
    target_object_id: Annotated[str, "Globally unique identifier for the target object in Asana."],
    enable_pretty_output: Annotated[
        bool | None,
        "Set to true for readable, indented output. Use primarily for debugging due to increased response size.",  # noqa: E501
    ] = None,
    filter_by_user: Annotated[
        str | None, "User identifier to filter requests. Accepts 'me', an email, or a user gid."
    ] = None,
    include_optional_properties: Annotated[
        list[str] | None, "Comma-separated list of optional properties to include in the response."
    ] = None,
) -> Annotated[dict[str, Any], "Response from the API endpoint 'getAccessRequests'."]:
    """Fetch pending access requests for a target object.

    Use this tool to retrieve pending access requests for a specific target object in Asana. You can also filter these requests by a specific user if needed."""  # noqa: E501
    request_data: Any = None
    if isinstance(request_data, dict):
        request_data = remove_none_values(request_data)
    content = json.dumps(request_data) if request_data else None
    response = await make_request(
        url="https://app.asana.com/api/1.0/access_requests",
        method="GET",
        params=remove_none_values({
            "target": target_object_id,
            "user": filter_by_user,
            "opt_pretty": enable_pretty_output,
            "opt_fields": include_optional_properties,
        }),
        headers=remove_none_values({
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            )
        }),
        content=content,
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(requires_auth=OAuth2(id="arcade-asana"))
async def submit_access_request_asana(
    context: ToolContext,
    access_request_message: Annotated[
        str | None,
        "Optional message providing context or additional information for the access request.",
    ] = None,
    target_gid: Annotated[
        str | None, "The GID of the project or portfolio you are requesting access to in Asana."
    ] = None,
) -> Annotated[dict[str, Any], "Response from the API endpoint 'createAccessRequest'."]:
    """Submit a new access request for Asana projects or portfolios.

    This tool is used to submit a new access request for accessing a private project or portfolio in Asana. It should be called when a user needs permission to access these private objects."""  # noqa: E501
    request_data: Any = {"data": {"target": target_gid, "message": access_request_message}}
    if isinstance(request_data, dict):
        request_data = remove_none_values(request_data)
    content = json.dumps(request_data) if request_data else None
    response = await make_request(
        url="https://app.asana.com/api/1.0/access_requests",
        method="POST",
        params=remove_none_values({}),
        headers=remove_none_values({
            "Content-Type": "application/json",
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            ),
        }),
        content=content,
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(requires_auth=OAuth2(id="arcade-asana"))
async def approve_access_request(
    context: ToolContext,
    access_request_global_id: Annotated[
        str, "Globally unique identifier for the specific access request to approve."
    ],
) -> Annotated[dict[str, Any], "Response from the API endpoint 'approveAccessRequest'."]:
    """Approves an access request for a target object in Asana.

    Use this tool to approve an access request for a specific target object in Asana. It should be called when a user needs to grant access to a specified request."""  # noqa: E501
    request_data: Any = None
    if isinstance(request_data, dict):
        request_data = remove_none_values(request_data)
    content = json.dumps(request_data) if request_data else None
    response = await make_request(
        url="https://app.asana.com/api/1.0/access_requests/{access_request_gid}/approve".format(  # noqa: UP032
            access_request_gid=access_request_global_id
        ),
        method="POST",
        params=remove_none_values({}),
        headers=remove_none_values({
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            )
        }),
        content=content,
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(requires_auth=OAuth2(id="arcade-asana"))
async def reject_access_request(
    context: ToolContext,
    access_request_identifier: Annotated[
        str,
        "Globally unique identifier for the access request to be rejected. This value is required for identifying which access request to reject in Asana.",  # noqa: E501
    ],
) -> Annotated[dict[str, Any], "Response from the API endpoint 'rejectAccessRequest'."]:
    """Reject an access request for a target object.

    This tool should be called when you need to reject a pending access request for a specific target object in Asana. Use it to manage access control effectively by denying requests as needed."""  # noqa: E501
    request_data: Any = None
    if isinstance(request_data, dict):
        request_data = remove_none_values(request_data)
    content = json.dumps(request_data) if request_data else None
    response = await make_request(
        url="https://app.asana.com/api/1.0/access_requests/{access_request_gid}/reject".format(  # noqa: UP032
            access_request_gid=access_request_identifier
        ),
        method="POST",
        params=remove_none_values({}),
        headers=remove_none_values({
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            )
        }),
        content=content,
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(requires_auth=OAuth2(id="arcade-asana"))
async def get_allocation_record(
    context: ToolContext,
    allocation_unique_id: Annotated[
        str, "Globally unique identifier for the allocation to fetch the complete record."
    ],
    enable_pretty_output: Annotated[
        bool | None,
        "Set to true to receive the response in a pretty, readable format. Useful for debugging, but increases response size and time.",  # noqa: E501
    ] = None,
    include_optional_properties: Annotated[
        list[str] | None,
        "Comma-separated list of optional properties to include in the response, as some properties are excluded by default.",  # noqa: E501
    ] = None,
) -> Annotated[dict[str, Any], "Response from the API endpoint 'getAllocation'."]:
    """Fetch the complete allocation record for a given ID.

    This tool provides detailed information about an allocation using its unique ID. It should be called when you need to access specific allocation data from Asana."""  # noqa: E501
    request_data: Any = None
    if isinstance(request_data, dict):
        request_data = remove_none_values(request_data)
    content = json.dumps(request_data) if request_data else None
    response = await make_request(
        url="https://app.asana.com/api/1.0/allocations/{allocation_gid}".format(  # noqa: UP032
            allocation_gid=allocation_unique_id
        ),
        method="GET",
        params=remove_none_values({
            "opt_pretty": enable_pretty_output,
            "opt_fields": include_optional_properties,
        }),
        headers=remove_none_values({
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            )
        }),
        content=content,
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(requires_auth=OAuth2(id="arcade-asana"))
async def update_allocation(
    context: ToolContext,
    mode: Annotated[
        ToolMode,
        "Operation mode: 'get_request_schema' returns the OpenAPI spec "
        "for the request body, 'execute' performs the actual operation",
    ],
    allocation_global_id: Annotated[
        str | None,
        "Globally unique identifier for the allocation to be updated. This is required to specify which allocation record to modify.  Required when mode is 'execute', ignored when mode is 'get_request_schema'.",  # noqa: E501
    ] = None,
    include_optional_fields: Annotated[
        list[str] | None,
        "Comma-separated list of optional properties to include in the response.  Only used when mode is 'execute'.",  # noqa: E501
    ] = None,
    enable_pretty_output: Annotated[
        bool | None,
        "Set to true to receive the response in a more readable, formatted JSON output. This increases response size and should be used mainly for debugging purposes.  Only used when mode is 'execute'.",  # noqa: E501
    ] = None,
    request_body: Annotated[
        str | None,
        "Stringified JSON representing the request body. Required when "
        "mode is 'execute', ignored when mode is 'get_request_schema'",
    ] = None,
) -> Annotated[dict[str, Any], "Response from the API endpoint 'updateAllocation'."]:
    """Update an existing allocation in Asana.

    Use this tool to update an existing allocation in Asana by providing specific fields to be changed. Unspecified fields will remain unchanged.

    Note: Understanding the request schema is necessary to properly create
    the stringified JSON input object for execution.\n\nThis operation also requires path, query parameters.

    Modes:
    - GET_REQUEST_SCHEMA: Returns the schema. Only call if you don't
      already have it. Do NOT call repeatedly if you already received
      the schema.
    - EXECUTE: Performs the operation with the provided request body
      JSON.\n      Note: You must also provide the required path, query parameters when executing.

    If you need the schema, call with mode='get_request_schema' ONCE, then execute.
    """  # noqa: E501
    if mode == ToolMode.GET_REQUEST_SCHEMA:
        return {
            "request_body_schema": REQUEST_BODY_SCHEMAS["UPDATEALLOCATION"],
            "instructions": (
                "Use the request_body_schema to construct a valid JSON object. "
                "Once you have populated the object following the schema "
                "structure and requirements, call this tool again with "
                "mode='execute' and the stringified JSON as the "
                "request_body parameter along with the required path, query parameters. "
                "Do NOT call the schema mode again - you already have "
                "the schema now."
            ),
        }

    # Mode is EXECUTE - validate parameters
    # Validate required parameters
    missing_params = []
    if not allocation_global_id:
        missing_params.append(("allocation_global_id", "path"))

    if missing_params:
        param_names = [p[0] for p in missing_params]
        param_details = ", ".join([f"{p[0]} ({p[1]})" for p in missing_params])
        raise RetryableToolError(
            message=f"Missing required parameters: {param_names}",
            developer_message=(f"Required parameters validation failed: {param_details}"),
            additional_prompt_content=(
                f"The following required parameters are missing: "
                f"{param_details}. Please call this tool again with all "
                "required parameters."
            ),
        )

    # Validate request body is provided (not None or empty string)
    # Note: Empty objects like {} are allowed - schema validation will check if valid
    if request_body is None or request_body.strip() == "":
        raise RetryableToolError(
            message="Request body is required when mode is 'execute'",
            developer_message="The request_body parameter was null or empty string",
            additional_prompt_content=(
                "The request body is required to perform this operation. "
                "Use the schema below to construct a valid JSON object, "
                "then call this tool again in execute mode with the "
                "stringified JSON as the request_body parameter.\n\n"
                "Schema:\n\n```json\n" + REQUEST_BODY_SCHEMAS["UPDATEALLOCATION"] + "\n```"
            ),
        )

    # Parse JSON
    try:
        request_data = json.loads(request_body)
    except json.JSONDecodeError as e:
        raise RetryableToolError(
            message=f"Invalid JSON in request body: {e!s}",
            developer_message=f"JSON parsing failed: {e!s}",
            additional_prompt_content=(
                f"The request body contains invalid JSON. Error: {e!s}\n\n"
                "Please provide a valid JSON string that matches the schema "
                "below, then call this tool again in execute mode.\n\n"
                "Schema:\n\n```json\n" + REQUEST_BODY_SCHEMAS["UPDATEALLOCATION"] + "\n```"
            ),
        ) from e

    response = await make_request_with_schema_validation(
        url="https://app.asana.com/api/1.0/allocations/{allocation_gid}".format(  # noqa: UP032
            allocation_gid=allocation_global_id
        ),
        method="PUT",
        request_data=request_data,
        schema=REQUEST_BODY_SCHEMAS["UPDATEALLOCATION"],
        params=remove_none_values({
            "opt_pretty": enable_pretty_output,
            "opt_fields": include_optional_fields,
        }),
        headers=remove_none_values({
            "Content-Type": "application/json",
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            ),
        }),
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(requires_auth=OAuth2(id="arcade-asana"))
async def delete_allocation(
    context: ToolContext,
    allocation_id: Annotated[str, "Globally unique identifier for the allocation to be deleted."],
    pretty_formatting: Annotated[
        bool | None,
        "Provide pretty JSON formatting for better readability, recommended for debugging.",
    ] = None,
) -> Annotated[dict[str, Any], "Response from the API endpoint 'deleteAllocation'."]:
    """Deletes a specific allocation in Asana.

    Use this tool to delete an existing allocation in Asana by providing the allocation ID. Useful for tasks where removing resource allocations is needed."""  # noqa: E501
    request_data: Any = None
    if isinstance(request_data, dict):
        request_data = remove_none_values(request_data)
    content = json.dumps(request_data) if request_data else None
    response = await make_request(
        url="https://app.asana.com/api/1.0/allocations/{allocation_gid}".format(  # noqa: UP032
            allocation_gid=allocation_id
        ),
        method="DELETE",
        params=remove_none_values({"opt_pretty": pretty_formatting}),
        headers=remove_none_values({
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            )
        }),
        content=content,
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(requires_auth=OAuth2(id="arcade-asana"))
async def get_project_allocations(
    context: ToolContext,
    assignee_id: Annotated[
        str | None,
        "Globally unique identifier for the user or placeholder the allocation is assigned to.",
    ] = None,
    enable_pretty_output: Annotated[
        bool | None,
        "Enable pretty output format for JSON. Use this for easier reading during debugging, as it increases response size and processing time.",  # noqa: E501
    ] = None,
    include_optional_properties: Annotated[
        list[str] | None,
        "List properties to include in the response, provided as a list of strings. Provides access to additional data fields.",  # noqa: E501
    ] = None,
    pagination_offset: Annotated[
        str | None,
        "Offset token for pagination. Use this to retrieve the next page of results. If not provided, the first page will be returned. Only use offsets returned from previous requests.",  # noqa: E501
    ] = None,
    project_id: Annotated[
        str | None, "Globally unique identifier for the project to filter allocations by."
    ] = None,
    results_per_page: Annotated[
        int | None, "The number of allocations to return per page, between 1 and 100."
    ] = None,
    workspace_id: Annotated[
        str | None, "Globally unique identifier for the workspace to filter allocations by."
    ] = None,
) -> Annotated[dict[str, Any], "Response from the API endpoint 'getAllocations'."]:
    """Retrieve allocations for a specific project, user, or placeholder.

    Use this tool to get a list of work allocations filtered by a specific project, user, or placeholder in Asana."""  # noqa: E501
    request_data: Any = None
    if isinstance(request_data, dict):
        request_data = remove_none_values(request_data)
    content = json.dumps(request_data) if request_data else None
    response = await make_request(
        url="https://app.asana.com/api/1.0/allocations",
        method="GET",
        params=remove_none_values({
            "opt_pretty": enable_pretty_output,
            "parent": project_id,
            "assignee": assignee_id,
            "workspace": workspace_id,
            "limit": results_per_page,
            "offset": pagination_offset,
            "opt_fields": include_optional_properties,
        }),
        headers=remove_none_values({
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            )
        }),
        content=content,
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(requires_auth=OAuth2(id="arcade-asana"))
async def create_allocation(
    context: ToolContext,
    mode: Annotated[
        ToolMode,
        "Operation mode: 'get_request_schema' returns the OpenAPI spec "
        "for the request body, 'execute' performs the actual operation",
    ],
    optional_properties_to_include: Annotated[
        list[str] | None,
        "List the properties to include that are not included by default in the allocation resource response. Provide as an array of strings.  Only used when mode is 'execute'.",  # noqa: E501
    ] = None,
    enable_pretty_output: Annotated[
        bool | None,
        "Enable pretty formatting for the response to improve readability with line breaks and indentation. Recommended only for debugging due to increased response time and size.  Only used when mode is 'execute'.",  # noqa: E501
    ] = None,
    request_body: Annotated[
        str | None,
        "Stringified JSON representing the request body. Required when "
        "mode is 'execute', ignored when mode is 'get_request_schema'",
    ] = None,
) -> Annotated[dict[str, Any], "Response from the API endpoint 'createAllocation'."]:
    """Creates a new allocation in Asana and returns its details.



    Note: Understanding the request schema is necessary to properly create
    the stringified JSON input object for execution.\n\nThis operation also requires query parameters.

    Modes:
    - GET_REQUEST_SCHEMA: Returns the schema. Only call if you don't
      already have it. Do NOT call repeatedly if you already received
      the schema.
    - EXECUTE: Performs the operation with the provided request body
      JSON.\n      Note: You must also provide the required query parameters when executing.

    If you need the schema, call with mode='get_request_schema' ONCE, then execute.
    """  # noqa: E501
    if mode == ToolMode.GET_REQUEST_SCHEMA:
        return {
            "request_body_schema": REQUEST_BODY_SCHEMAS["CREATEALLOCATION"],
            "instructions": (
                "Use the request_body_schema to construct a valid JSON object. "
                "Once you have populated the object following the schema "
                "structure and requirements, call this tool again with "
                "mode='execute' and the stringified JSON as the "
                "request_body parameter along with the required query parameters. "
                "Do NOT call the schema mode again - you already have "
                "the schema now."
            ),
        }

    # Mode is EXECUTE - validate parameters

    # Validate request body is provided (not None or empty string)
    # Note: Empty objects like {} are allowed - schema validation will check if valid
    if request_body is None or request_body.strip() == "":
        raise RetryableToolError(
            message="Request body is required when mode is 'execute'",
            developer_message="The request_body parameter was null or empty string",
            additional_prompt_content=(
                "The request body is required to perform this operation. "
                "Use the schema below to construct a valid JSON object, "
                "then call this tool again in execute mode with the "
                "stringified JSON as the request_body parameter.\n\n"
                "Schema:\n\n```json\n" + REQUEST_BODY_SCHEMAS["CREATEALLOCATION"] + "\n```"
            ),
        )

    # Parse JSON
    try:
        request_data = json.loads(request_body)
    except json.JSONDecodeError as e:
        raise RetryableToolError(
            message=f"Invalid JSON in request body: {e!s}",
            developer_message=f"JSON parsing failed: {e!s}",
            additional_prompt_content=(
                f"The request body contains invalid JSON. Error: {e!s}\n\n"
                "Please provide a valid JSON string that matches the schema "
                "below, then call this tool again in execute mode.\n\n"
                "Schema:\n\n```json\n" + REQUEST_BODY_SCHEMAS["CREATEALLOCATION"] + "\n```"
            ),
        ) from e

    response = await make_request_with_schema_validation(
        url="https://app.asana.com/api/1.0/allocations",
        method="POST",
        request_data=request_data,
        schema=REQUEST_BODY_SCHEMAS["CREATEALLOCATION"],
        params=remove_none_values({
            "opt_pretty": enable_pretty_output,
            "opt_fields": optional_properties_to_include,
        }),
        headers=remove_none_values({
            "Content-Type": "application/json",
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            ),
        }),
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(requires_auth=OAuth2(id="arcade-asana", scopes=["attachments:read"]))
async def fetch_attachment_details(
    context: ToolContext,
    attachment_unique_id: Annotated[
        str, "The globally unique identifier for the attachment in Asana."
    ],
    enable_pretty_output: Annotated[
        bool | None,
        "Set to true for pretty-printed JSON output. Increases response size and time; recommended for debugging.",  # noqa: E501
    ] = None,
    include_optional_properties: Annotated[
        list[str] | None,
        "An array of properties to include in the response. Specify which additional fields you want in the returned attachment record.",  # noqa: E501
    ] = None,
) -> Annotated[dict[str, Any], "Response from the API endpoint 'getAttachment'."]:
    """Fetch the full record of a specific attachment.

    Retrieves detailed information for a single attachment in Asana. Use this tool to access complete attachment records by attachment ID."""  # noqa: E501
    request_data: Any = None
    if isinstance(request_data, dict):
        request_data = remove_none_values(request_data)
    content = json.dumps(request_data) if request_data else None
    response = await make_request(
        url="https://app.asana.com/api/1.0/attachments/{attachment_gid}".format(  # noqa: UP032
            attachment_gid=attachment_unique_id
        ),
        method="GET",
        params=remove_none_values({
            "opt_pretty": enable_pretty_output,
            "opt_fields": include_optional_properties,
        }),
        headers=remove_none_values({
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            )
        }),
        content=content,
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(requires_auth=OAuth2(id="arcade-asana", scopes=["attachments:delete"]))
async def delete_attachment(
    context: ToolContext,
    attachment_unique_id: Annotated[
        str, "Globally unique identifier for the attachment to be deleted."
    ],
    pretty_output_enabled: Annotated[
        bool | None,
        "Set to true to return the response in a readable format with line breaks and indentation. Use for debugging, as it increases response time and size.",  # noqa: E501
    ] = None,
) -> Annotated[dict[str, Any], "Response from the API endpoint 'deleteAttachment'."]:
    """Delete a specific attachment in Asana.

    Use this tool to delete an existing attachment in Asana by providing the attachment's unique ID. Requires `attachments:delete` scope."""  # noqa: E501
    request_data: Any = None
    if isinstance(request_data, dict):
        request_data = remove_none_values(request_data)
    content = json.dumps(request_data) if request_data else None
    response = await make_request(
        url="https://app.asana.com/api/1.0/attachments/{attachment_gid}".format(  # noqa: UP032
            attachment_gid=attachment_unique_id
        ),
        method="DELETE",
        params=remove_none_values({"opt_pretty": pretty_output_enabled}),
        headers=remove_none_values({
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            )
        }),
        content=content,
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(requires_auth=OAuth2(id="arcade-asana", scopes=["attachments:read"]))
async def get_attachments(
    context: ToolContext,
    object_gid: Annotated[
        str,
        "Globally unique identifier for the Asana object to fetch attachments from, such as a `project`, `project_brief`, or `task`.",  # noqa: E501
    ],
    enable_pretty_output: Annotated[
        bool | None,
        "Return the response in a readable format with line breaks and indentation. Recommended for debugging as it may increase response size and time.",  # noqa: E501
    ] = None,
    include_optional_properties: Annotated[
        list[str] | None,
        "Comma-separated list of properties to include, which are excluded by default.",
    ] = None,
    pagination_offset_token: Annotated[
        str | None,
        "Token for pagination. Use this to get the next page of results. If omitted, the first page is returned. Use only tokens from previous responses.",  # noqa: E501
    ] = None,
    results_per_page: Annotated[
        int | None, "Number of objects to return per page, must be between 1 and 100."
    ] = None,
) -> Annotated[dict[str, Any], "Response from the API endpoint 'getAttachmentsForObject'."]:
    """Retrieve all attachments for a specified Asana object.

    This tool returns all attachments associated with a specified Asana object. It can be used to get attachments for projects, project briefs, and tasks. For projects, it provides files from the \"Key resources\" section; for project briefs, inline files in the brief itself; for tasks, all associated files, including inline images in descriptions."""  # noqa: E501
    request_data: Any = None
    if isinstance(request_data, dict):
        request_data = remove_none_values(request_data)
    content = json.dumps(request_data) if request_data else None
    response = await make_request(
        url="https://app.asana.com/api/1.0/attachments",
        method="GET",
        params=remove_none_values({
            "opt_pretty": enable_pretty_output,
            "limit": results_per_page,
            "offset": pagination_offset_token,
            "parent": object_gid,
            "opt_fields": include_optional_properties,
        }),
        headers=remove_none_values({
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            )
        }),
        content=content,
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(requires_auth=OAuth2(id="arcade-asana"))
async def retrieve_audit_log_events(
    context: ToolContext,
    workspace_unique_id: Annotated[
        str,
        "Globally unique identifier for the workspace or organization to filter the audit log events.",  # noqa: E501
    ],
    actor_id_filter: Annotated[
        str | None, "Filter events to those triggered by the actor with this unique ID."
    ] = None,
    actor_type_filter: Annotated[
        str | None, "Specify the actor type to filter events. Use only if not querying by actor ID."
    ] = None,
    event_type_filter: Annotated[
        str | None,
        "Specify the type of events to filter. Refer to the supported audit log events for valid types.",  # noqa: E501
    ] = None,
    filter_by_resource_id: Annotated[
        str | None,
        "Filter events based on the specific resource ID to retrieve only those associated with this ID.",  # noqa: E501
    ] = None,
    filter_events_end_time: Annotated[
        str | None,
        "Filter events to include only those created before this date and time (exclusive).",
    ] = None,
    pagination_offset: Annotated[
        str | None,
        "Offset token to specify the starting point for retrieving the next page of results. Use the token from the previous response to continue paging through results. Leaving this unset will fetch the first page.",  # noqa: E501
    ] = None,
    results_per_page: Annotated[
        int | None,
        "Set the number of audit log events to return per page of results, between 1 and 100.",
    ] = None,
    start_time_filter: Annotated[
        str | None,
        "Filter events created on or after this time (inclusive). Provide in ISO 8601 format.",
    ] = None,
) -> Annotated[dict[str, Any], "Response from the API endpoint 'getAuditLogEvents'."]:
    """Retrieve audit log events from your Asana domain.

    Use this tool to get a list of audit log events captured in your Asana domain. It supports various query parameters for filtering events and provides paginated results. Useful for tracking and auditing activities since October 8th, 2021."""  # noqa: E501
    request_data: Any = None
    if isinstance(request_data, dict):
        request_data = remove_none_values(request_data)
    content = json.dumps(request_data) if request_data else None
    response = await make_request(
        url="https://app.asana.com/api/1.0/workspaces/{workspace_gid}/audit_log_events".format(  # noqa: UP032
            workspace_gid=workspace_unique_id
        ),
        method="GET",
        params=remove_none_values({
            "start_at": start_time_filter,
            "end_at": filter_events_end_time,
            "event_type": event_type_filter,
            "actor_type": actor_type_filter,
            "actor_gid": actor_id_filter,
            "resource_gid": filter_by_resource_id,
            "limit": results_per_page,
            "offset": pagination_offset,
        }),
        headers=remove_none_values({
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            )
        }),
        content=content,
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(requires_auth=OAuth2(id="arcade-asana"))
async def create_parallel_requests_asana(
    context: ToolContext,
    mode: Annotated[
        ToolMode,
        "Operation mode: 'get_request_schema' returns the OpenAPI spec "
        "for the request body, 'execute' performs the actual operation",
    ],
    include_optional_fields: Annotated[
        list[str] | None,
        "A list of optional properties to include in the response. Provide as an array of strings.  Only used when mode is 'execute'.",  # noqa: E501
    ] = None,
    enable_pretty_output: Annotated[
        bool | None,
        "Enable pretty output format with line breaks and indentation. Useful for debugging but may increase response size and time.  Only used when mode is 'execute'.",  # noqa: E501
    ] = None,
    request_body: Annotated[
        str | None,
        "Stringified JSON representing the request body. Required when "
        "mode is 'execute', ignored when mode is 'get_request_schema'",
    ] = None,
) -> Annotated[dict[str, Any], "Response from the API endpoint 'createBatchRequest'."]:
    """Execute multiple requests to Asana's API simultaneously.

    Use this tool to make parallel requests to Asana's API efficiently, allowing multiple operations to be executed at once.

    Note: Understanding the request schema is necessary to properly create
    the stringified JSON input object for execution.\n\nThis operation also requires query parameters.

    Modes:
    - GET_REQUEST_SCHEMA: Returns the schema. Only call if you don't
      already have it. Do NOT call repeatedly if you already received
      the schema.
    - EXECUTE: Performs the operation with the provided request body
      JSON.\n      Note: You must also provide the required query parameters when executing.

    If you need the schema, call with mode='get_request_schema' ONCE, then execute.
    """  # noqa: E501
    if mode == ToolMode.GET_REQUEST_SCHEMA:
        return {
            "request_body_schema": REQUEST_BODY_SCHEMAS["CREATEPARALLELREQUESTSASANA"],
            "instructions": (
                "Use the request_body_schema to construct a valid JSON object. "
                "Once you have populated the object following the schema "
                "structure and requirements, call this tool again with "
                "mode='execute' and the stringified JSON as the "
                "request_body parameter along with the required query parameters. "
                "Do NOT call the schema mode again - you already have "
                "the schema now."
            ),
        }

    # Mode is EXECUTE - validate parameters

    # Validate request body is provided (not None or empty string)
    # Note: Empty objects like {} are allowed - schema validation will check if valid
    if request_body is None or request_body.strip() == "":
        raise RetryableToolError(
            message="Request body is required when mode is 'execute'",
            developer_message="The request_body parameter was null or empty string",
            additional_prompt_content=(
                "The request body is required to perform this operation. "
                "Use the schema below to construct a valid JSON object, "
                "then call this tool again in execute mode with the "
                "stringified JSON as the request_body parameter.\n\n"
                "Schema:\n\n```json\n"
                + REQUEST_BODY_SCHEMAS["CREATEPARALLELREQUESTSASANA"]
                + "\n```"
            ),
        )

    # Parse JSON
    try:
        request_data = json.loads(request_body)
    except json.JSONDecodeError as e:
        raise RetryableToolError(
            message=f"Invalid JSON in request body: {e!s}",
            developer_message=f"JSON parsing failed: {e!s}",
            additional_prompt_content=(
                f"The request body contains invalid JSON. Error: {e!s}\n\n"
                "Please provide a valid JSON string that matches the schema "
                "below, then call this tool again in execute mode.\n\n"
                "Schema:\n\n```json\n"
                + REQUEST_BODY_SCHEMAS["CREATEPARALLELREQUESTSASANA"]
                + "\n```"
            ),
        ) from e

    response = await make_request_with_schema_validation(
        url="https://app.asana.com/api/1.0/batch",
        method="POST",
        request_data=request_data,
        schema=REQUEST_BODY_SCHEMAS["CREATEPARALLELREQUESTSASANA"],
        params=remove_none_values({
            "opt_pretty": enable_pretty_output,
            "opt_fields": include_optional_fields,
        }),
        headers=remove_none_values({
            "Content-Type": "application/json",
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            ),
        }),
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(requires_auth=OAuth2(id="arcade-asana"))
async def get_custom_field_settings_for_project(
    context: ToolContext,
    project_id: Annotated[
        str, "Globally unique identifier for the Asana project to retrieve custom field settings."
    ],
    enable_pretty_output: Annotated[
        bool | None,
        "Enable to receive a pretty, human-readable response with line breaks and indentation. Increases response time and size, use for debugging.",  # noqa: E501
    ] = None,
    include_optional_fields: Annotated[
        list[str] | None,
        "Comma-separated list of property names to include in the response, expanding beyond the default fields.",  # noqa: E501
    ] = None,
    pagination_offset_token: Annotated[
        str | None,
        "An offset token for pagination. Use the token from a previous request to get the next page of results. If not provided, it returns the first page.",  # noqa: E501
    ] = None,
    results_per_page: Annotated[
        int | None,
        "Specify the number of custom field settings to return per page, between 1 and 100.",
    ] = None,
) -> Annotated[
    dict[str, Any], "Response from the API endpoint 'getCustomFieldSettingsForProject'."
]:
    """Get custom field settings for a specified project.

    Use this tool to retrieve all custom field settings associated with a specific project in Asana. It returns a list in compact form, which can be expanded using `opt_fields` to include additional data."""  # noqa: E501
    request_data: Any = None
    if isinstance(request_data, dict):
        request_data = remove_none_values(request_data)
    content = json.dumps(request_data) if request_data else None
    response = await make_request(
        url="https://app.asana.com/api/1.0/projects/{project_gid}/custom_field_settings".format(  # noqa: UP032
            project_gid=project_id
        ),
        method="GET",
        params=remove_none_values({
            "opt_pretty": enable_pretty_output,
            "limit": results_per_page,
            "offset": pagination_offset_token,
            "opt_fields": include_optional_fields,
        }),
        headers=remove_none_values({
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            )
        }),
        content=content,
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(requires_auth=OAuth2(id="arcade-asana"))
async def get_portfolio_custom_field_settings(
    context: ToolContext,
    portfolio_unique_id: Annotated[str, "Globally unique identifier for the portfolio in Asana."],
    include_optional_fields: Annotated[
        list[str] | None,
        "List optional properties to include in the response, specified as an array of strings.",
    ] = None,
    pagination_offset_token: Annotated[
        str | None,
        "Offset token to fetch the next page of results. Use a token returned from a previous request. If not provided, the first page of results will be returned.",  # noqa: E501
    ] = None,
    pretty_output: Annotated[
        bool | None,
        "Set to true for pretty-formatted JSON output, with line breaks and indentation, mainly for debugging purposes.",  # noqa: E501
    ] = None,
    results_per_page: Annotated[
        int | None, "Specify the number of results to return per page. Must be between 1 and 100."
    ] = None,
) -> Annotated[
    dict[str, Any], "Response from the API endpoint 'getCustomFieldSettingsForPortfolio'."
]:
    """Retrieve custom field settings for an Asana portfolio.

    Call this tool to get all the custom field settings associated with a specific portfolio on Asana in compact form."""  # noqa: E501
    request_data: Any = None
    if isinstance(request_data, dict):
        request_data = remove_none_values(request_data)
    content = json.dumps(request_data) if request_data else None
    response = await make_request(
        url="https://app.asana.com/api/1.0/portfolios/{portfolio_gid}/custom_field_settings".format(  # noqa: UP032
            portfolio_gid=portfolio_unique_id
        ),
        method="GET",
        params=remove_none_values({
            "opt_pretty": pretty_output,
            "limit": results_per_page,
            "offset": pagination_offset_token,
            "opt_fields": include_optional_fields,
        }),
        headers=remove_none_values({
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            )
        }),
        content=content,
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(requires_auth=OAuth2(id="arcade-asana", scopes=["custom_fields:write"]))
async def create_custom_field_in_workspace(
    context: ToolContext,
    mode: Annotated[
        ToolMode,
        "Operation mode: 'get_request_schema' returns the OpenAPI spec "
        "for the request body, 'execute' performs the actual operation",
    ],
    include_optional_properties: Annotated[
        list[str] | None,
        "A list of optional property names to include in the response. Ensure these do not conflict with defaults.  Only used when mode is 'execute'.",  # noqa: E501
    ] = None,
    enable_pretty_output: Annotated[
        bool | None,
        "Set to true to receive a JSON response with line breaks and indentation for better readability, mainly for debugging purposes.  Only used when mode is 'execute'.",  # noqa: E501
    ] = None,
    request_body: Annotated[
        str | None,
        "Stringified JSON representing the request body. Required when "
        "mode is 'execute', ignored when mode is 'get_request_schema'",
    ] = None,
) -> Annotated[dict[str, Any], "Response from the API endpoint 'createCustomField'."]:
    """Create a new custom field in an Asana workspace.

    Use this tool to create a custom field in a specific Asana workspace. The field's name must be unique and the type can be text, enum, multi_enum, number, date, or people. The workspace cannot be changed once set.

    Note: Understanding the request schema is necessary to properly create
    the stringified JSON input object for execution.\n\nThis operation also requires query parameters.

    Modes:
    - GET_REQUEST_SCHEMA: Returns the schema. Only call if you don't
      already have it. Do NOT call repeatedly if you already received
      the schema.
    - EXECUTE: Performs the operation with the provided request body
      JSON.\n      Note: You must also provide the required query parameters when executing.

    If you need the schema, call with mode='get_request_schema' ONCE, then execute.
    """  # noqa: E501
    if mode == ToolMode.GET_REQUEST_SCHEMA:
        return {
            "request_body_schema": REQUEST_BODY_SCHEMAS["CREATECUSTOMFIELDINWORKSPACE"],
            "instructions": (
                "Use the request_body_schema to construct a valid JSON object. "
                "Once you have populated the object following the schema "
                "structure and requirements, call this tool again with "
                "mode='execute' and the stringified JSON as the "
                "request_body parameter along with the required query parameters. "
                "Do NOT call the schema mode again - you already have "
                "the schema now."
            ),
        }

    # Mode is EXECUTE - validate parameters

    # Validate request body is provided (not None or empty string)
    # Note: Empty objects like {} are allowed - schema validation will check if valid
    if request_body is None or request_body.strip() == "":
        raise RetryableToolError(
            message="Request body is required when mode is 'execute'",
            developer_message="The request_body parameter was null or empty string",
            additional_prompt_content=(
                "The request body is required to perform this operation. "
                "Use the schema below to construct a valid JSON object, "
                "then call this tool again in execute mode with the "
                "stringified JSON as the request_body parameter.\n\n"
                "Schema:\n\n```json\n"
                + REQUEST_BODY_SCHEMAS["CREATECUSTOMFIELDINWORKSPACE"]
                + "\n```"
            ),
        )

    # Parse JSON
    try:
        request_data = json.loads(request_body)
    except json.JSONDecodeError as e:
        raise RetryableToolError(
            message=f"Invalid JSON in request body: {e!s}",
            developer_message=f"JSON parsing failed: {e!s}",
            additional_prompt_content=(
                f"The request body contains invalid JSON. Error: {e!s}\n\n"
                "Please provide a valid JSON string that matches the schema "
                "below, then call this tool again in execute mode.\n\n"
                "Schema:\n\n```json\n"
                + REQUEST_BODY_SCHEMAS["CREATECUSTOMFIELDINWORKSPACE"]
                + "\n```"
            ),
        ) from e

    response = await make_request_with_schema_validation(
        url="https://app.asana.com/api/1.0/custom_fields",
        method="POST",
        request_data=request_data,
        schema=REQUEST_BODY_SCHEMAS["CREATECUSTOMFIELDINWORKSPACE"],
        params=remove_none_values({
            "opt_pretty": enable_pretty_output,
            "opt_fields": include_optional_properties,
        }),
        headers=remove_none_values({
            "Content-Type": "application/json",
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            ),
        }),
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(requires_auth=OAuth2(id="arcade-asana", scopes=["custom_fields:read"]))
async def get_custom_field_metadata(
    context: ToolContext,
    custom_field_id: Annotated[
        str, "Globally unique identifier for the custom field in Asana to retrieve its metadata."
    ],
    enable_pretty_output: Annotated[
        bool | None,
        "Enable pretty printing for the output, making it more readable. Ideal for debugging, but increases response time and size.",  # noqa: E501
    ] = None,
    include_optional_properties: Annotated[
        list[str] | None, "Comma-separated list of optional properties to include in the response."
    ] = None,
) -> Annotated[dict[str, Any], "Response from the API endpoint 'getCustomField'."]:
    """Retrieve complete metadata of a custom field in Asana.

    Fetches the full definition of a custom field's metadata in Asana, including type-specific details such as enum options."""  # noqa: E501
    request_data: Any = None
    if isinstance(request_data, dict):
        request_data = remove_none_values(request_data)
    content = json.dumps(request_data) if request_data else None
    response = await make_request(
        url="https://app.asana.com/api/1.0/custom_fields/{custom_field_gid}".format(  # noqa: UP032
            custom_field_gid=custom_field_id
        ),
        method="GET",
        params=remove_none_values({
            "opt_pretty": enable_pretty_output,
            "opt_fields": include_optional_properties,
        }),
        headers=remove_none_values({
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            )
        }),
        content=content,
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(requires_auth=OAuth2(id="arcade-asana", scopes=["custom_fields:write"]))
async def update_asana_custom_field(
    context: ToolContext,
    mode: Annotated[
        ToolMode,
        "Operation mode: 'get_request_schema' returns the OpenAPI spec "
        "for the request body, 'execute' performs the actual operation",
    ],
    custom_field_global_id: Annotated[
        str | None,
        "Globally unique identifier for the custom field to be updated.  Required when mode is 'execute', ignored when mode is 'get_request_schema'.",  # noqa: E501
    ] = None,
    include_optional_properties: Annotated[
        list[str] | None,
        "Comma-separated list of optional properties to include in the response, as the endpoint excludes some by default.  Only used when mode is 'execute'.",  # noqa: E501
    ] = None,
    pretty_output: Annotated[
        bool | None,
        "Set to true to receive a pretty-formatted output with line breaks and indentation. Increases response time and size, suitable for debugging.  Only used when mode is 'execute'.",  # noqa: E501
    ] = None,
    request_body: Annotated[
        str | None,
        "Stringified JSON representing the request body. Required when "
        "mode is 'execute', ignored when mode is 'get_request_schema'",
    ] = None,
) -> Annotated[dict[str, Any], "Response from the API endpoint 'updateCustomField'."]:
    """Update specific fields of an Asana custom field.

    Use this tool to update an existing Asana custom field. Only the specified fields in the request will be updated, leaving other fields unchanged. The custom field's type and enum options cannot be modified using this endpoint. Ensure the 'custom_fields:write' scope is granted when invoking this method. Locked fields can only be updated by the user who locked them.

    Note: Understanding the request schema is necessary to properly create
    the stringified JSON input object for execution.\n\nThis operation also requires path, query parameters.

    Modes:
    - GET_REQUEST_SCHEMA: Returns the schema. Only call if you don't
      already have it. Do NOT call repeatedly if you already received
      the schema.
    - EXECUTE: Performs the operation with the provided request body
      JSON.\n      Note: You must also provide the required path, query parameters when executing.

    If you need the schema, call with mode='get_request_schema' ONCE, then execute.
    """  # noqa: E501
    if mode == ToolMode.GET_REQUEST_SCHEMA:
        return {
            "request_body_schema": REQUEST_BODY_SCHEMAS["UPDATEASANACUSTOMFIELD"],
            "instructions": (
                "Use the request_body_schema to construct a valid JSON object. "
                "Once you have populated the object following the schema "
                "structure and requirements, call this tool again with "
                "mode='execute' and the stringified JSON as the "
                "request_body parameter along with the required path, query parameters. "
                "Do NOT call the schema mode again - you already have "
                "the schema now."
            ),
        }

    # Mode is EXECUTE - validate parameters
    # Validate required parameters
    missing_params = []
    if not custom_field_global_id:
        missing_params.append(("custom_field_global_id", "path"))

    if missing_params:
        param_names = [p[0] for p in missing_params]
        param_details = ", ".join([f"{p[0]} ({p[1]})" for p in missing_params])
        raise RetryableToolError(
            message=f"Missing required parameters: {param_names}",
            developer_message=(f"Required parameters validation failed: {param_details}"),
            additional_prompt_content=(
                f"The following required parameters are missing: "
                f"{param_details}. Please call this tool again with all "
                "required parameters."
            ),
        )

    # Validate request body is provided (not None or empty string)
    # Note: Empty objects like {} are allowed - schema validation will check if valid
    if request_body is None or request_body.strip() == "":
        raise RetryableToolError(
            message="Request body is required when mode is 'execute'",
            developer_message="The request_body parameter was null or empty string",
            additional_prompt_content=(
                "The request body is required to perform this operation. "
                "Use the schema below to construct a valid JSON object, "
                "then call this tool again in execute mode with the "
                "stringified JSON as the request_body parameter.\n\n"
                "Schema:\n\n```json\n" + REQUEST_BODY_SCHEMAS["UPDATEASANACUSTOMFIELD"] + "\n```"
            ),
        )

    # Parse JSON
    try:
        request_data = json.loads(request_body)
    except json.JSONDecodeError as e:
        raise RetryableToolError(
            message=f"Invalid JSON in request body: {e!s}",
            developer_message=f"JSON parsing failed: {e!s}",
            additional_prompt_content=(
                f"The request body contains invalid JSON. Error: {e!s}\n\n"
                "Please provide a valid JSON string that matches the schema "
                "below, then call this tool again in execute mode.\n\n"
                "Schema:\n\n```json\n" + REQUEST_BODY_SCHEMAS["UPDATEASANACUSTOMFIELD"] + "\n```"
            ),
        ) from e

    response = await make_request_with_schema_validation(
        url="https://app.asana.com/api/1.0/custom_fields/{custom_field_gid}".format(  # noqa: UP032
            custom_field_gid=custom_field_global_id
        ),
        method="PUT",
        request_data=request_data,
        schema=REQUEST_BODY_SCHEMAS["UPDATEASANACUSTOMFIELD"],
        params=remove_none_values({
            "opt_pretty": pretty_output,
            "opt_fields": include_optional_properties,
        }),
        headers=remove_none_values({
            "Content-Type": "application/json",
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            ),
        }),
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(requires_auth=OAuth2(id="arcade-asana"))
async def delete_custom_field(
    context: ToolContext,
    custom_field_id: Annotated[
        str, "The unique global identifier for the custom field to be deleted."
    ],
    pretty_output: Annotated[
        bool | None, "Set to true for a formatted and indented response; mainly for debugging."
    ] = None,
) -> Annotated[dict[str, Any], "Response from the API endpoint 'deleteCustomField'."]:
    """Delete a specific custom field in Asana.

    Use this tool to delete a specific custom field in Asana. Locked custom fields can only be deleted by the user who locked them. It returns confirmation of the deletion."""  # noqa: E501
    request_data: Any = None
    if isinstance(request_data, dict):
        request_data = remove_none_values(request_data)
    content = json.dumps(request_data) if request_data else None
    response = await make_request(
        url="https://app.asana.com/api/1.0/custom_fields/{custom_field_gid}".format(  # noqa: UP032
            custom_field_gid=custom_field_id
        ),
        method="DELETE",
        params=remove_none_values({"opt_pretty": pretty_output}),
        headers=remove_none_values({
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            )
        }),
        content=content,
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(requires_auth=OAuth2(id="arcade-asana", scopes=["custom_fields:read"]))
async def get_custom_fields_for_workspace(
    context: ToolContext,
    workspace_identifier: Annotated[
        str, "Globally unique identifier for the workspace or organization in Asana."
    ],
    enable_pretty_output: Annotated[
        bool | None,
        "Enable pretty printed output for the response, useful for debugging. This increases response time and size.",  # noqa: E501
    ] = None,
    include_optional_properties: Annotated[
        list[str] | None,
        "A list of properties to include in the response for a workspace's custom fields. Include values such as 'name', 'type', etc.",  # noqa: E501
    ] = None,
    pagination_offset_token: Annotated[
        str | None,
        "An offset token for pagination. Use a token from a previous request to continue where it left off. If not used, retrieves the first page.",  # noqa: E501
    ] = None,
    results_per_page: Annotated[
        int | None, "Specifies the number of custom fields to return per page, between 1 and 100."
    ] = None,
) -> Annotated[dict[str, Any], "Response from the API endpoint 'getCustomFieldsForWorkspace'."]:
    """Retrieve custom fields for a specific workspace.

    Fetches a list of all custom fields in a specified workspace, using Asana's API. Requires the 'custom_fields:read' scope."""  # noqa: E501
    request_data: Any = None
    if isinstance(request_data, dict):
        request_data = remove_none_values(request_data)
    content = json.dumps(request_data) if request_data else None
    response = await make_request(
        url="https://app.asana.com/api/1.0/workspaces/{workspace_gid}/custom_fields".format(  # noqa: UP032
            workspace_gid=workspace_identifier
        ),
        method="GET",
        params=remove_none_values({
            "opt_pretty": enable_pretty_output,
            "limit": results_per_page,
            "offset": pagination_offset_token,
            "opt_fields": include_optional_properties,
        }),
        headers=remove_none_values({
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            )
        }),
        content=content,
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(requires_auth=OAuth2(id="arcade-asana", scopes=["custom_fields:write"]))
async def add_enum_option_to_custom_field(
    context: ToolContext,
    mode: Annotated[
        ToolMode,
        "Operation mode: 'get_request_schema' returns the OpenAPI spec "
        "for the request body, 'execute' performs the actual operation",
    ],
    custom_field_identifier: Annotated[
        str | None,
        "Globally unique identifier for the custom field in Asana. Use this to specify the field you want to modify.  Required when mode is 'execute', ignored when mode is 'get_request_schema'.",  # noqa: E501
    ] = None,
    include_optional_properties: Annotated[
        list[str] | None,
        "List of properties to include in the response that are excluded by default. Provide these as an array of strings.  Only used when mode is 'execute'.",  # noqa: E501
    ] = None,
    enable_pretty_output: Annotated[
        bool | None,
        "Enable to receive a readable JSON response with line breaks and indentation. Use primarily for debugging as it increases response size and time.  Only used when mode is 'execute'.",  # noqa: E501
    ] = None,
    request_body: Annotated[
        str | None,
        "Stringified JSON representing the request body. Required when "
        "mode is 'execute', ignored when mode is 'get_request_schema'",
    ] = None,
) -> Annotated[dict[str, Any], "Response from the API endpoint 'createEnumOptionForCustomField'."]:
    """Add an enum option to a custom field in Asana.

    This tool adds a new enum option to a custom field in Asana, updating the list of enum options. Use this when managing custom fields and needing to expand the choices available. Requires 'custom_fields:write' scope.

    Note: Understanding the request schema is necessary to properly create
    the stringified JSON input object for execution.\n\nThis operation also requires path, query parameters.

    Modes:
    - GET_REQUEST_SCHEMA: Returns the schema. Only call if you don't
      already have it. Do NOT call repeatedly if you already received
      the schema.
    - EXECUTE: Performs the operation with the provided request body
      JSON.\n      Note: You must also provide the required path, query parameters when executing.

    If you need the schema, call with mode='get_request_schema' ONCE, then execute.
    """  # noqa: E501
    if mode == ToolMode.GET_REQUEST_SCHEMA:
        return {
            "request_body_schema": REQUEST_BODY_SCHEMAS["ADDENUMOPTIONTOCUSTOMFIELD"],
            "instructions": (
                "Use the request_body_schema to construct a valid JSON object. "
                "Once you have populated the object following the schema "
                "structure and requirements, call this tool again with "
                "mode='execute' and the stringified JSON as the "
                "request_body parameter along with the required path, query parameters. "
                "Do NOT call the schema mode again - you already have "
                "the schema now."
            ),
        }

    # Mode is EXECUTE - validate parameters
    # Validate required parameters
    missing_params = []
    if not custom_field_identifier:
        missing_params.append(("custom_field_identifier", "path"))

    if missing_params:
        param_names = [p[0] for p in missing_params]
        param_details = ", ".join([f"{p[0]} ({p[1]})" for p in missing_params])
        raise RetryableToolError(
            message=f"Missing required parameters: {param_names}",
            developer_message=(f"Required parameters validation failed: {param_details}"),
            additional_prompt_content=(
                f"The following required parameters are missing: "
                f"{param_details}. Please call this tool again with all "
                "required parameters."
            ),
        )

    # Validate request body is provided (not None or empty string)
    # Note: Empty objects like {} are allowed - schema validation will check if valid
    if request_body is None or request_body.strip() == "":
        raise RetryableToolError(
            message="Request body is required when mode is 'execute'",
            developer_message="The request_body parameter was null or empty string",
            additional_prompt_content=(
                "The request body is required to perform this operation. "
                "Use the schema below to construct a valid JSON object, "
                "then call this tool again in execute mode with the "
                "stringified JSON as the request_body parameter.\n\n"
                "Schema:\n\n```json\n"
                + REQUEST_BODY_SCHEMAS["ADDENUMOPTIONTOCUSTOMFIELD"]
                + "\n```"
            ),
        )

    # Parse JSON
    try:
        request_data = json.loads(request_body)
    except json.JSONDecodeError as e:
        raise RetryableToolError(
            message=f"Invalid JSON in request body: {e!s}",
            developer_message=f"JSON parsing failed: {e!s}",
            additional_prompt_content=(
                f"The request body contains invalid JSON. Error: {e!s}\n\n"
                "Please provide a valid JSON string that matches the schema "
                "below, then call this tool again in execute mode.\n\n"
                "Schema:\n\n```json\n"
                + REQUEST_BODY_SCHEMAS["ADDENUMOPTIONTOCUSTOMFIELD"]
                + "\n```"
            ),
        ) from e

    response = await make_request_with_schema_validation(
        url="https://app.asana.com/api/1.0/custom_fields/{custom_field_gid}/enum_options".format(  # noqa: UP032
            custom_field_gid=custom_field_identifier
        ),
        method="POST",
        request_data=request_data,
        schema=REQUEST_BODY_SCHEMAS["ADDENUMOPTIONTOCUSTOMFIELD"],
        params=remove_none_values({
            "opt_pretty": enable_pretty_output,
            "opt_fields": include_optional_properties,
        }),
        headers=remove_none_values({
            "Content-Type": "application/json",
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            ),
        }),
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(requires_auth=OAuth2(id="arcade-asana", scopes=["custom_fields:write"]))
async def reorder_enum_option_custom_field(
    context: ToolContext,
    custom_field_id: Annotated[
        str, "Globally unique identifier for the custom field to be modified."
    ],
    enable_pretty_output: Annotated[
        bool | None,
        "Enable pretty output for a more readable JSON response format, useful for debugging.",
    ] = None,
    enum_option_gid_to_relocate: Annotated[
        str | None,
        "The globally unique identifier (GID) of the enum option to relocate within the custom field.",  # noqa: E501
    ] = None,
    include_optional_properties: Annotated[
        list[str] | None,
        "A list of resource properties to include in the response, which are excluded by default. This should be a list of strings.",  # noqa: E501
    ] = None,
    insert_after_enum_option: Annotated[
        str | None,
        "The ID of an existing enum option after which the new option should be inserted. Cannot be used with 'insert_before_enum_option'.",  # noqa: E501
    ] = None,
    insert_before_enum_option_gid: Annotated[
        str | None,
        "GID of an existing enum option before which the new option should be inserted. Cannot be used with after_enum_option.",  # noqa: E501
    ] = None,
) -> Annotated[dict[str, Any], "Response from the API endpoint 'insertEnumOptionForCustomField'."]:
    """Reorder enum options in a custom field.

    Moves an enum option within a custom field to a specified position relative to another option. Locked fields can only be edited by the locker."""  # noqa: E501
    request_data: Any = {
        "data": {
            "enum_option": enum_option_gid_to_relocate,
            "before_enum_option": insert_before_enum_option_gid,
            "after_enum_option": insert_after_enum_option,
        }
    }
    if isinstance(request_data, dict):
        request_data = remove_none_values(request_data)
    content = json.dumps(request_data) if request_data else None
    response = await make_request(
        url="https://app.asana.com/api/1.0/custom_fields/{custom_field_gid}/enum_options/insert".format(  # noqa: UP032
            custom_field_gid=custom_field_id
        ),
        method="POST",
        params=remove_none_values({
            "opt_pretty": enable_pretty_output,
            "opt_fields": include_optional_properties,
        }),
        headers=remove_none_values({
            "Content-Type": "application/json",
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            ),
        }),
        content=content,
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(requires_auth=OAuth2(id="arcade-asana", scopes=["custom_fields:write"]))
async def update_enum_option(
    context: ToolContext,
    enum_option_gid: Annotated[
        str, "The globally unique identifier for the enum option to update."
    ],
    enum_option_color: Annotated[
        str | None, "The color of the enum option. Defaults to 'none' if not provided."
    ] = None,
    enum_option_enabled: Annotated[
        bool | None,
        "Indicates if the enum option is selectable for the custom field. Provide 'true' to make it selectable, 'false' otherwise.",  # noqa: E501
    ] = None,
    enum_option_name: Annotated[str | None, "The name of the enum option to be updated."] = None,
    include_optional_properties: Annotated[
        list[str] | None,
        "Provide a list of properties to include in the response, beyond the default set.",
    ] = None,
    pretty_output: Annotated[
        bool | None,
        "Set to true to receive the response in a pretty-printed format. Increases response time and size, useful for debugging.",  # noqa: E501
    ] = None,
    resource_gid: Annotated[
        str | None, "Globally unique identifier for the resource as a string."
    ] = None,
    resource_type: Annotated[
        str | None,
        "The base type of the resource, specified as a string. This is necessary for updating the enum option.",  # noqa: E501
    ] = None,
) -> Annotated[dict[str, Any], "Response from the API endpoint 'updateEnumOption'."]:
    """Update an existing enum option in Asana custom fields.

    This tool is used to update an existing enum option in Asana custom fields. It's necessary to have the 'custom_fields:write' scope. Locked custom fields can only be updated by the user who locked them."""  # noqa: E501
    request_data: Any = {
        "data": {
            "gid": resource_gid,
            "resource_type": resource_type,
            "name": enum_option_name,
            "enabled": enum_option_enabled,
            "color": enum_option_color,
        }
    }
    if isinstance(request_data, dict):
        request_data = remove_none_values(request_data)
    content = json.dumps(request_data) if request_data else None
    response = await make_request(
        url="https://app.asana.com/api/1.0/enum_options/{enum_option_gid}".format(  # noqa: UP032
            enum_option_gid=enum_option_gid
        ),
        method="PUT",
        params=remove_none_values({
            "opt_pretty": pretty_output,
            "opt_fields": include_optional_properties,
        }),
        headers=remove_none_values({
            "Content-Type": "application/json",
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            ),
        }),
        content=content,
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(requires_auth=OAuth2(id="arcade-asana"))
async def get_custom_types(
    context: ToolContext,
    project_id: Annotated[
        str, "Globally unique identifier for the project to filter custom types."
    ],
    enable_pretty_output: Annotated[
        bool | None,
        "Set to true for pretty-printed JSON output during debugging. Increases response size and time.",  # noqa: E501
    ] = None,
    include_optional_properties: Annotated[
        list[str] | None,
        "Comma-separated list of properties to include in the response for additional details.",
    ] = None,
    next_page_offset_token: Annotated[
        str | None,
        "Offset token from a previous response to fetch the next page of results. If not provided, the first page will be returned.",  # noqa: E501
    ] = None,
    results_per_page: Annotated[
        int | None, "Number of custom types to return per page. Must be between 1 and 100."
    ] = None,
) -> Annotated[dict[str, Any], "Response from the API endpoint 'getCustomTypes'."]:
    """Retrieve all custom types for a project in Asana.

    Use this tool to get a list of custom types linked to a project in Asana. Customize returned data using `opt_fields` for more detailed information."""  # noqa: E501
    request_data: Any = None
    if isinstance(request_data, dict):
        request_data = remove_none_values(request_data)
    content = json.dumps(request_data) if request_data else None
    response = await make_request(
        url="https://app.asana.com/api/1.0/custom_types",
        method="GET",
        params=remove_none_values({
            "project": project_id,
            "opt_pretty": enable_pretty_output,
            "limit": results_per_page,
            "offset": next_page_offset_token,
            "opt_fields": include_optional_properties,
        }),
        headers=remove_none_values({
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            )
        }),
        content=content,
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(requires_auth=OAuth2(id="arcade-asana"))
async def get_custom_type_details(
    context: ToolContext,
    custom_type_id: Annotated[str, "Globally unique identifier for the custom type in Asana."],
    enable_pretty_output: Annotated[
        bool | None,
        "If true, the response is formatted with line breaks and indentation for readability. Use this for debugging; it may increase response time and size.",  # noqa: E501
    ] = None,
    include_optional_fields: Annotated[
        list[str] | None,
        "A list of property names to include in the response for the custom type. These are optional fields that are not included by default.",  # noqa: E501
    ] = None,
) -> Annotated[dict[str, Any], "Response from the API endpoint 'getCustomType'."]:
    """Retrieve complete details of a specific custom type in Asana.

    Use this to obtain the full record for a custom type by its ID. Useful for accessing detailed information about custom types within Asana projects."""  # noqa: E501
    request_data: Any = None
    if isinstance(request_data, dict):
        request_data = remove_none_values(request_data)
    content = json.dumps(request_data) if request_data else None
    response = await make_request(
        url="https://app.asana.com/api/1.0/custom_types/{custom_type_gid}".format(  # noqa: UP032
            custom_type_gid=custom_type_id
        ),
        method="GET",
        params=remove_none_values({
            "opt_pretty": enable_pretty_output,
            "opt_fields": include_optional_fields,
        }),
        headers=remove_none_values({
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            )
        }),
        content=content,
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(requires_auth=OAuth2(id="arcade-asana"))
async def fetch_asana_events(
    context: ToolContext,
    target_resource_id: Annotated[
        str, "The ID of the resource to subscribe to. It can be a task, project, or goal."
    ],
    enable_pretty_output: Annotated[
        bool | None,
        "Enable pretty JSON output with line breaks and indentation for readability; primarily for debugging.",  # noqa: E501
    ] = None,
    include_optional_properties: Annotated[
        list[str] | None,
        "Comma-separated list of property names to include in the response for more detailed event records.",  # noqa: E501
    ] = None,
    sync_token: Annotated[
        str | None,
        "A sync token from the last request, or omit on the first sync to receive events from the start point. Ensure to handle the token for continued syncing.",  # noqa: E501
    ] = None,
) -> Annotated[dict[str, Any], "Response from the API endpoint 'getEvents'."]:
    """Fetches detailed records of recent Asana events.

    This tool retrieves a complete record of all events in Asana that occurred since a specific sync token was created. It should be called when you need to obtain the latest activities or changes related to a resource. If more than 100 events exist, the response will indicate additional events are available."""  # noqa: E501
    request_data: Any = None
    if isinstance(request_data, dict):
        request_data = remove_none_values(request_data)
    content = json.dumps(request_data) if request_data else None
    response = await make_request(
        url="https://app.asana.com/api/1.0/events",
        method="GET",
        params=remove_none_values({
            "resource": target_resource_id,
            "sync": sync_token,
            "opt_pretty": enable_pretty_output,
            "opt_fields": include_optional_properties,
        }),
        headers=remove_none_values({
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            )
        }),
        content=content,
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(requires_auth=OAuth2(id="arcade-asana"))
async def initiate_graph_export(
    context: ToolContext,
    parent_object_id: Annotated[
        str | None,
        "Globally unique ID of the Asana parent object: goal, project, portfolio, or team.",
    ] = None,
) -> Annotated[dict[str, Any], "Response from the API endpoint 'createGraphExport'."]:
    """Initiate a graph export job for Asana objects.

    Initiates a graph export job for a specified Asana goal, team, portfolio, or project. The export is processed asynchronously. Use the jobs endpoint to monitor progress. Caching occurs for exports exceeding 1,000 tasks, lasting 4 hours."""  # noqa: E501
    request_data: Any = {"data": {"parent": parent_object_id}}
    if isinstance(request_data, dict):
        request_data = remove_none_values(request_data)
    content = json.dumps(request_data) if request_data else None
    response = await make_request(
        url="https://app.asana.com/api/1.0/exports/graph",
        method="POST",
        params=remove_none_values({}),
        headers=remove_none_values({
            "Content-Type": "application/json",
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            ),
        }),
        content=content,
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(requires_auth=OAuth2(id="arcade-asana"))
async def initiate_bulk_resource_export(
    context: ToolContext,
    mode: Annotated[
        ToolMode,
        "Operation mode: 'get_request_schema' returns the OpenAPI spec "
        "for the request body, 'execute' performs the actual operation",
    ],
    request_body: Annotated[
        str | None,
        "Stringified JSON representing the request body. Required when "
        "mode is 'execute', ignored when mode is 'get_request_schema'",
    ] = None,
) -> Annotated[dict[str, Any], "Response from the API endpoint 'createResourceExport'."]:
    """Initiate a bulk export of tasks, teams, or messages in a workspace.

    Use this tool to start exporting tasks, teams, or messages from a workspace in Asana. The export is processed asynchronously, and results can be monitored using the jobs endpoint. The export includes attachments and stories by default, with options to specify fields and apply filters. The final export file is in JSON Lines format, compressed in a gzip container.

    Note: Understanding the request schema is necessary to properly create
    the stringified JSON input object for execution.

    Modes:
    - GET_REQUEST_SCHEMA: Returns the schema. Only call if you don't
      already have it. Do NOT call repeatedly if you already received
      the schema.
    - EXECUTE: Performs the operation with the provided request body
      JSON.

    If you need the schema, call with mode='get_request_schema' ONCE, then execute.
    """  # noqa: E501
    if mode == ToolMode.GET_REQUEST_SCHEMA:
        return {
            "request_body_schema": REQUEST_BODY_SCHEMAS["INITIATEBULKRESOURCEEXPORT"],
            "instructions": (
                "Use the request_body_schema to construct a valid JSON object. "
                "Once you have populated the object following the schema "
                "structure and requirements, call this tool again with "
                "mode='execute' and the stringified JSON as the "
                "request_body parameter. "
                "Do NOT call the schema mode again - you already have "
                "the schema now."
            ),
        }

    # Mode is EXECUTE - validate parameters

    # Validate request body is provided (not None or empty string)
    # Note: Empty objects like {} are allowed - schema validation will check if valid
    if request_body is None or request_body.strip() == "":
        raise RetryableToolError(
            message="Request body is required when mode is 'execute'",
            developer_message="The request_body parameter was null or empty string",
            additional_prompt_content=(
                "The request body is required to perform this operation. "
                "Use the schema below to construct a valid JSON object, "
                "then call this tool again in execute mode with the "
                "stringified JSON as the request_body parameter.\n\n"
                "Schema:\n\n```json\n"
                + REQUEST_BODY_SCHEMAS["INITIATEBULKRESOURCEEXPORT"]
                + "\n```"
            ),
        )

    # Parse JSON
    try:
        request_data = json.loads(request_body)
    except json.JSONDecodeError as e:
        raise RetryableToolError(
            message=f"Invalid JSON in request body: {e!s}",
            developer_message=f"JSON parsing failed: {e!s}",
            additional_prompt_content=(
                f"The request body contains invalid JSON. Error: {e!s}\n\n"
                "Please provide a valid JSON string that matches the schema "
                "below, then call this tool again in execute mode.\n\n"
                "Schema:\n\n```json\n"
                + REQUEST_BODY_SCHEMAS["INITIATEBULKRESOURCEEXPORT"]
                + "\n```"
            ),
        ) from e

    response = await make_request_with_schema_validation(
        url="https://app.asana.com/api/1.0/exports/resource",
        method="POST",
        request_data=request_data,
        schema=REQUEST_BODY_SCHEMAS["INITIATEBULKRESOURCEEXPORT"],
        params=remove_none_values({}),
        headers=remove_none_values({
            "Content-Type": "application/json",
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            ),
        }),
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(requires_auth=OAuth2(id="arcade-asana"))
async def get_goal_relationship_details(
    context: ToolContext,
    goal_relationship_identifier: Annotated[
        str, "Globally unique identifier for the goal relationship you want to retrieve."
    ],
    enable_pretty_output: Annotated[
        bool | None,
        "Enables pretty formatting for the response, increasing readability but also response size and time. Use for debugging.",  # noqa: E501
    ] = None,
    include_optional_properties: Annotated[
        list[str] | None,
        "A comma-separated list of optional properties to include in the response. Use this to include properties that are not returned by default.",  # noqa: E501
    ] = None,
) -> Annotated[dict[str, Any], "Response from the API endpoint 'getGoalRelationship'."]:
    """Retrieve details of a specific Asana goal relationship.

    Use this tool to get the complete updated record of a specific goal relationship in Asana by providing the goal relationship ID."""  # noqa: E501
    request_data: Any = None
    if isinstance(request_data, dict):
        request_data = remove_none_values(request_data)
    content = json.dumps(request_data) if request_data else None
    response = await make_request(
        url="https://app.asana.com/api/1.0/goal_relationships/{goal_relationship_gid}".format(  # noqa: UP032
            goal_relationship_gid=goal_relationship_identifier
        ),
        method="GET",
        params=remove_none_values({
            "opt_pretty": enable_pretty_output,
            "opt_fields": include_optional_properties,
        }),
        headers=remove_none_values({
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            )
        }),
        content=content,
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(requires_auth=OAuth2(id="arcade-asana"))
async def update_goal_relationship(
    context: ToolContext,
    mode: Annotated[
        ToolMode,
        "Operation mode: 'get_request_schema' returns the OpenAPI spec "
        "for the request body, 'execute' performs the actual operation",
    ],
    goal_relationship_unique_id: Annotated[
        str | None,
        "The globally unique identifier for the specific goal relationship to be updated.  Required when mode is 'execute', ignored when mode is 'get_request_schema'.",  # noqa: E501
    ] = None,
    include_optional_properties: Annotated[
        list[str] | None,
        "Specify properties to include in the response. Provide as a list of property names, which will include optional fields in the returned goal relationship.  Only used when mode is 'execute'.",  # noqa: E501
    ] = None,
    enable_pretty_output: Annotated[
        bool | None,
        "Set to true for a readable response with indentation. Increases response size/time, recommended for debugging.  Only used when mode is 'execute'.",  # noqa: E501
    ] = None,
    request_body: Annotated[
        str | None,
        "Stringified JSON representing the request body. Required when "
        "mode is 'execute', ignored when mode is 'get_request_schema'",
    ] = None,
) -> Annotated[dict[str, Any], "Response from the API endpoint 'updateGoalRelationship'."]:
    """Update an existing goal relationship in Asana.

    Use this tool to update specific fields of an existing goal relationship in Asana. Provide only the fields you wish to change, as unspecified fields will remain unchanged.

    Note: Understanding the request schema is necessary to properly create
    the stringified JSON input object for execution.\n\nThis operation also requires path, query parameters.

    Modes:
    - GET_REQUEST_SCHEMA: Returns the schema. Only call if you don't
      already have it. Do NOT call repeatedly if you already received
      the schema.
    - EXECUTE: Performs the operation with the provided request body
      JSON.\n      Note: You must also provide the required path, query parameters when executing.

    If you need the schema, call with mode='get_request_schema' ONCE, then execute.
    """  # noqa: E501
    if mode == ToolMode.GET_REQUEST_SCHEMA:
        return {
            "request_body_schema": REQUEST_BODY_SCHEMAS["UPDATEGOALRELATIONSHIP"],
            "instructions": (
                "Use the request_body_schema to construct a valid JSON object. "
                "Once you have populated the object following the schema "
                "structure and requirements, call this tool again with "
                "mode='execute' and the stringified JSON as the "
                "request_body parameter along with the required path, query parameters. "
                "Do NOT call the schema mode again - you already have "
                "the schema now."
            ),
        }

    # Mode is EXECUTE - validate parameters
    # Validate required parameters
    missing_params = []
    if not goal_relationship_unique_id:
        missing_params.append(("goal_relationship_unique_id", "path"))

    if missing_params:
        param_names = [p[0] for p in missing_params]
        param_details = ", ".join([f"{p[0]} ({p[1]})" for p in missing_params])
        raise RetryableToolError(
            message=f"Missing required parameters: {param_names}",
            developer_message=(f"Required parameters validation failed: {param_details}"),
            additional_prompt_content=(
                f"The following required parameters are missing: "
                f"{param_details}. Please call this tool again with all "
                "required parameters."
            ),
        )

    # Validate request body is provided (not None or empty string)
    # Note: Empty objects like {} are allowed - schema validation will check if valid
    if request_body is None or request_body.strip() == "":
        raise RetryableToolError(
            message="Request body is required when mode is 'execute'",
            developer_message="The request_body parameter was null or empty string",
            additional_prompt_content=(
                "The request body is required to perform this operation. "
                "Use the schema below to construct a valid JSON object, "
                "then call this tool again in execute mode with the "
                "stringified JSON as the request_body parameter.\n\n"
                "Schema:\n\n```json\n" + REQUEST_BODY_SCHEMAS["UPDATEGOALRELATIONSHIP"] + "\n```"
            ),
        )

    # Parse JSON
    try:
        request_data = json.loads(request_body)
    except json.JSONDecodeError as e:
        raise RetryableToolError(
            message=f"Invalid JSON in request body: {e!s}",
            developer_message=f"JSON parsing failed: {e!s}",
            additional_prompt_content=(
                f"The request body contains invalid JSON. Error: {e!s}\n\n"
                "Please provide a valid JSON string that matches the schema "
                "below, then call this tool again in execute mode.\n\n"
                "Schema:\n\n```json\n" + REQUEST_BODY_SCHEMAS["UPDATEGOALRELATIONSHIP"] + "\n```"
            ),
        ) from e

    response = await make_request_with_schema_validation(
        url="https://app.asana.com/api/1.0/goal_relationships/{goal_relationship_gid}".format(  # noqa: UP032
            goal_relationship_gid=goal_relationship_unique_id
        ),
        method="PUT",
        request_data=request_data,
        schema=REQUEST_BODY_SCHEMAS["UPDATEGOALRELATIONSHIP"],
        params=remove_none_values({
            "opt_pretty": enable_pretty_output,
            "opt_fields": include_optional_properties,
        }),
        headers=remove_none_values({
            "Content-Type": "application/json",
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            ),
        }),
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(requires_auth=OAuth2(id="arcade-asana"))
async def get_goal_relationships(
    context: ToolContext,
    supported_goal_id: Annotated[
        str, "Globally unique identifier for the supported goal in the goal relationship."
    ],
    enable_pretty_output: Annotated[
        bool | None,
        "Set to true for pretty JSON output with indentation. Use mainly for debugging as it increases response size and time.",  # noqa: E501
    ] = None,
    goal_relationship_resource_subtype: Annotated[
        str | None, "Filter goal relationships by a specific resource subtype."
    ] = None,
    include_optional_properties: Annotated[
        list[str] | None,
        "Comma-separated list of optional properties to include in the response. Allows access to additional fields excluded by default.",  # noqa: E501
    ] = None,
    pagination_offset: Annotated[
        str | None,
        "Token to specify the starting point for the next page of results. Use the token from the previous response to continue pagination.",  # noqa: E501
    ] = None,
    results_per_page: Annotated[
        int | None,
        "Specify the number of goal relationship records to return per page. Must be between 1 and 100.",  # noqa: E501
    ] = None,
) -> Annotated[dict[str, Any], "Response from the API endpoint 'getGoalRelationships'."]:
    """Retrieve compact goal relationship records from Asana.

    Call this tool to get information on goal relationships within an Asana workspace. It provides a compact view of the connections between goals, which may be useful for tracking progress or understanding hierarchy."""  # noqa: E501
    request_data: Any = None
    if isinstance(request_data, dict):
        request_data = remove_none_values(request_data)
    content = json.dumps(request_data) if request_data else None
    response = await make_request(
        url="https://app.asana.com/api/1.0/goal_relationships",
        method="GET",
        params=remove_none_values({
            "opt_pretty": enable_pretty_output,
            "limit": results_per_page,
            "offset": pagination_offset,
            "supported_goal": supported_goal_id,
            "resource_subtype": goal_relationship_resource_subtype,
            "opt_fields": include_optional_properties,
        }),
        headers=remove_none_values({
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            )
        }),
        content=content,
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(requires_auth=OAuth2(id="arcade-asana"))
async def create_goal_supporting_relationship(
    context: ToolContext,
    goal_global_identifier: Annotated[
        str, "Globally unique identifier for the goal to which a supporting resource will be added."
    ],
    enable_pretty_output: Annotated[
        bool | None,
        "Enable to receive the response in a formatted and indented manner for easier readability. Recommended for debugging purposes as it increases response time and size.",  # noqa: E501
    ] = None,
    include_optional_fields: Annotated[
        list[str] | None,
        "Comma-separated list of optional resource properties to include in the response.",
    ] = None,
    insert_after_subgoal_id: Annotated[
        str | None,
        "ID of the subgoal to insert the new subgoal after. Cannot use with `insert_before`. Only for subgoal addition.",  # noqa: E501
    ] = None,
    insert_subgoal_before_id: Annotated[
        str | None,
        "The ID of an existing subgoal. The new subgoal will be placed before this subgoal. Cannot be used with `insert_after_subgoal_id`. Only for adding subgoals.",  # noqa: E501
    ] = None,
    supporting_goal_contribution_weight: Annotated[
        int | None,
        "A number between 0 and 1 indicating the supporting goal's contribution to the parent goal's progress. Defaults to 0.",  # noqa: E501
    ] = None,
    supporting_resource_id: Annotated[
        str | None,
        "The GID of the supporting resource to add to the parent goal. It must be the GID of a goal, project, task, or portfolio.",  # noqa: E501
    ] = None,
) -> Annotated[dict[str, Any], "Response from the API endpoint 'addSupportingRelationship'."]:
    """Add a supporting resource to a specific goal in Asana.

    Use this tool to create a relationship between a goal and a supporting resource in Asana by specifying the goal ID. This tool is useful when you need to associate additional resources with an existing goal."""  # noqa: E501
    request_data: Any = {
        "data": {
            "supporting_resource": supporting_resource_id,
            "insert_before": insert_subgoal_before_id,
            "insert_after": insert_after_subgoal_id,
            "contribution_weight": supporting_goal_contribution_weight,
        }
    }
    if isinstance(request_data, dict):
        request_data = remove_none_values(request_data)
    content = json.dumps(request_data) if request_data else None
    response = await make_request(
        url="https://app.asana.com/api/1.0/goals/{goal_gid}/addSupportingRelationship".format(  # noqa: UP032
            goal_gid=goal_global_identifier
        ),
        method="POST",
        params=remove_none_values({
            "opt_pretty": enable_pretty_output,
            "opt_fields": include_optional_fields,
        }),
        headers=remove_none_values({
            "Content-Type": "application/json",
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            ),
        }),
        content=content,
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(requires_auth=OAuth2(id="arcade-asana"))
async def remove_goal_relationship(
    context: ToolContext,
    goal_global_id: Annotated[
        str, "Globally unique identifier for the goal to identify which goal is being modified."
    ],
    provide_pretty_output: Annotated[
        bool | None,
        "Provides pretty-printed output for debugging, enhancing readability with line breaks and indentation. Increases response size and time.",  # noqa: E501
    ] = None,
    supporting_resource_gid: Annotated[
        str | None,
        "The globally unique identifier (gid) of the supporting resource (goal, project, task, or portfolio) to remove from the parent goal.",  # noqa: E501
    ] = None,
) -> Annotated[dict[str, Any], "Response from the API endpoint 'removeSupportingRelationship'."]:
    """Removes a supporting relationship from a goal in Asana.

    Use this tool to remove a specified supporting relationship from a parent goal in Asana. It is useful for managing and updating the hierarchical organization of goals."""  # noqa: E501
    request_data: Any = {"data": {"supporting_resource": supporting_resource_gid}}
    if isinstance(request_data, dict):
        request_data = remove_none_values(request_data)
    content = json.dumps(request_data) if request_data else None
    response = await make_request(
        url="https://app.asana.com/api/1.0/goals/{goal_gid}/removeSupportingRelationship".format(  # noqa: UP032
            goal_gid=goal_global_id
        ),
        method="POST",
        params=remove_none_values({"opt_pretty": provide_pretty_output}),
        headers=remove_none_values({
            "Content-Type": "application/json",
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            ),
        }),
        content=content,
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(requires_auth=OAuth2(id="arcade-asana", scopes=["goals:read"]))
async def get_asana_goal_details(
    context: ToolContext,
    goal_global_identifier: Annotated[
        str, "Globally unique identifier for the goal to be retrieved."
    ],
    enable_pretty_output: Annotated[
        bool | None,
        "If true, formats the response with line breaks and indentation for readability, increasing response size and time.",  # noqa: E501
    ] = None,
    include_optional_fields: Annotated[
        list[str] | None,
        "Specify optional properties to include in the response as a list of strings.",
    ] = None,
) -> Annotated[dict[str, Any], "Response from the API endpoint 'getGoal'."]:
    """Fetches detailed information for a specific Asana goal.

    This tool retrieves the complete record of a specified goal in Asana, including associated time periods and custom field settings. It requires the necessary permissions: 'goals:read', 'time_periods:read', and 'custom_fields:read'."""  # noqa: E501
    request_data: Any = None
    if isinstance(request_data, dict):
        request_data = remove_none_values(request_data)
    content = json.dumps(request_data) if request_data else None
    response = await make_request(
        url="https://app.asana.com/api/1.0/goals/{goal_gid}".format(  # noqa: UP032
            goal_gid=goal_global_identifier
        ),
        method="GET",
        params=remove_none_values({
            "opt_pretty": enable_pretty_output,
            "opt_fields": include_optional_fields,
        }),
        headers=remove_none_values({
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            )
        }),
        content=content,
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(requires_auth=OAuth2(id="arcade-asana"))
async def update_asana_goal(
    context: ToolContext,
    mode: Annotated[
        ToolMode,
        "Operation mode: 'get_request_schema' returns the OpenAPI spec "
        "for the request body, 'execute' performs the actual operation",
    ],
    goal_unique_id: Annotated[
        str | None,
        "Globally unique identifier for the goal to be updated.  Required when mode is 'execute', ignored when mode is 'get_request_schema'.",  # noqa: E501
    ] = None,
    included_goal_properties: Annotated[
        list[str] | None,
        "List of optional goal properties to include in the response. Provide as an array of strings.  Only used when mode is 'execute'.",  # noqa: E501
    ] = None,
    enable_pretty_output: Annotated[
        bool | None,
        "Set to true to format the response with line breaks and indentation for readability. Mainly for debugging as it increases response size.  Only used when mode is 'execute'.",  # noqa: E501
    ] = None,
    request_body: Annotated[
        str | None,
        "Stringified JSON representing the request body. Required when "
        "mode is 'execute', ignored when mode is 'get_request_schema'",
    ] = None,
) -> Annotated[dict[str, Any], "Response from the API endpoint 'updateGoal'."]:
    """Update a specific goal in Asana.

    Use this tool to update the details of an existing goal in Asana. Only the specified fields in the request will be changed, while others remain unchanged. It returns the fully updated goal record.

    Note: Understanding the request schema is necessary to properly create
    the stringified JSON input object for execution.\n\nThis operation also requires path, query parameters.

    Modes:
    - GET_REQUEST_SCHEMA: Returns the schema. Only call if you don't
      already have it. Do NOT call repeatedly if you already received
      the schema.
    - EXECUTE: Performs the operation with the provided request body
      JSON.\n      Note: You must also provide the required path, query parameters when executing.

    If you need the schema, call with mode='get_request_schema' ONCE, then execute.
    """  # noqa: E501
    if mode == ToolMode.GET_REQUEST_SCHEMA:
        return {
            "request_body_schema": REQUEST_BODY_SCHEMAS["UPDATEASANAGOAL"],
            "instructions": (
                "Use the request_body_schema to construct a valid JSON object. "
                "Once you have populated the object following the schema "
                "structure and requirements, call this tool again with "
                "mode='execute' and the stringified JSON as the "
                "request_body parameter along with the required path, query parameters. "
                "Do NOT call the schema mode again - you already have "
                "the schema now."
            ),
        }

    # Mode is EXECUTE - validate parameters
    # Validate required parameters
    missing_params = []
    if not goal_unique_id:
        missing_params.append(("goal_unique_id", "path"))

    if missing_params:
        param_names = [p[0] for p in missing_params]
        param_details = ", ".join([f"{p[0]} ({p[1]})" for p in missing_params])
        raise RetryableToolError(
            message=f"Missing required parameters: {param_names}",
            developer_message=(f"Required parameters validation failed: {param_details}"),
            additional_prompt_content=(
                f"The following required parameters are missing: "
                f"{param_details}. Please call this tool again with all "
                "required parameters."
            ),
        )

    # Validate request body is provided (not None or empty string)
    # Note: Empty objects like {} are allowed - schema validation will check if valid
    if request_body is None or request_body.strip() == "":
        raise RetryableToolError(
            message="Request body is required when mode is 'execute'",
            developer_message="The request_body parameter was null or empty string",
            additional_prompt_content=(
                "The request body is required to perform this operation. "
                "Use the schema below to construct a valid JSON object, "
                "then call this tool again in execute mode with the "
                "stringified JSON as the request_body parameter.\n\n"
                "Schema:\n\n```json\n" + REQUEST_BODY_SCHEMAS["UPDATEASANAGOAL"] + "\n```"
            ),
        )

    # Parse JSON
    try:
        request_data = json.loads(request_body)
    except json.JSONDecodeError as e:
        raise RetryableToolError(
            message=f"Invalid JSON in request body: {e!s}",
            developer_message=f"JSON parsing failed: {e!s}",
            additional_prompt_content=(
                f"The request body contains invalid JSON. Error: {e!s}\n\n"
                "Please provide a valid JSON string that matches the schema "
                "below, then call this tool again in execute mode.\n\n"
                "Schema:\n\n```json\n" + REQUEST_BODY_SCHEMAS["UPDATEASANAGOAL"] + "\n```"
            ),
        ) from e

    response = await make_request_with_schema_validation(
        url="https://app.asana.com/api/1.0/goals/{goal_gid}".format(goal_gid=goal_unique_id),  # noqa: UP032
        method="PUT",
        request_data=request_data,
        schema=REQUEST_BODY_SCHEMAS["UPDATEASANAGOAL"],
        params=remove_none_values({
            "opt_pretty": enable_pretty_output,
            "opt_fields": included_goal_properties,
        }),
        headers=remove_none_values({
            "Content-Type": "application/json",
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            ),
        }),
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(requires_auth=OAuth2(id="arcade-asana"))
async def delete_asana_goal(
    context: ToolContext,
    goal_identifier: Annotated[
        str, "The unique global identifier for the specific Asana goal to delete."
    ],
    enable_pretty_output: Annotated[
        bool | None,
        "If true, provides pretty JSON output with indentation and line breaks for readability. Recommended for debugging due to increased response size and time.",  # noqa: E501
    ] = None,
) -> Annotated[dict[str, Any], "Response from the API endpoint 'deleteGoal'."]:
    """Delete a specific goal in Asana.

    Use this tool to delete an existing goal in Asana by specifying the goal ID."""
    request_data: Any = None
    if isinstance(request_data, dict):
        request_data = remove_none_values(request_data)
    content = json.dumps(request_data) if request_data else None
    response = await make_request(
        url="https://app.asana.com/api/1.0/goals/{goal_gid}".format(goal_gid=goal_identifier),  # noqa: UP032
        method="DELETE",
        params=remove_none_values({"opt_pretty": enable_pretty_output}),
        headers=remove_none_values({
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            )
        }),
        content=content,
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(requires_auth=OAuth2(id="arcade-asana", scopes=["goals:read"]))
async def get_compact_goals(
    context: ToolContext,
    enable_pretty_output: Annotated[
        bool | None,
        "Enable pretty formatting for readable JSON response. Mainly for debugging; increases response time and size.",  # noqa: E501
    ] = None,
    included_optional_properties: Annotated[
        list[str] | None,
        "A list of specific properties to include in the response. Use a comma-separated list format.",  # noqa: E501
    ] = None,
    is_workspace_level: Annotated[
        bool | None,
        "Set to true to filter for goals where the workspace level is active. Must be used with the workspace parameter.",  # noqa: E501
    ] = None,
    pagination_offset_token: Annotated[
        str | None,
        "Offset token for pagination. Use the token from a previous response to retrieve the next page. If not provided, returns the first page.",  # noqa: E501
    ] = None,
    portfolio_id: Annotated[
        str | None, "Globally unique identifier for the supporting portfolio in Asana."
    ] = None,
    project_id: Annotated[
        str | None,
        "Globally unique identifier for the project. Used to filter goals associated with a specific project.",  # noqa: E501
    ] = None,
    results_per_page: Annotated[
        int | None, "The number of goal records to return per page. Must be between 1 and 100."
    ] = None,
    supporting_task_id: Annotated[
        str | None, "Globally unique identifier for the supporting task in Asana."
    ] = None,
    team_id: Annotated[
        str | None,
        "Globally unique identifier for the team. Use this to filter goals associated with a specific team.",  # noqa: E501
    ] = None,
    time_period_identifiers: Annotated[
        list[str] | None,
        "A list of globally unique identifiers for the desired time periods to filter the goals.",
    ] = None,
    workspace_id: Annotated[str | None, "Globally unique identifier for the workspace."] = None,
) -> Annotated[dict[str, Any], "Response from the API endpoint 'getGoals'."]:
    """Retrieve compact goal records from Asana.

    Use this tool to get a summary of goals available in Asana. It should be called when you need to access an overview of goals, requiring the 'goals:read' permission scope."""  # noqa: E501
    request_data: Any = None
    if isinstance(request_data, dict):
        request_data = remove_none_values(request_data)
    content = json.dumps(request_data) if request_data else None
    response = await make_request(
        url="https://app.asana.com/api/1.0/goals",
        method="GET",
        params=remove_none_values({
            "opt_pretty": enable_pretty_output,
            "portfolio": portfolio_id,
            "project": project_id,
            "task": supporting_task_id,
            "is_workspace_level": is_workspace_level,
            "team": team_id,
            "workspace": workspace_id,
            "time_periods": time_period_identifiers,
            "limit": results_per_page,
            "offset": pagination_offset_token,
            "opt_fields": included_optional_properties,
        }),
        headers=remove_none_values({
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            )
        }),
        content=content,
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(requires_auth=OAuth2(id="arcade-asana"))
async def create_asana_goal(
    context: ToolContext,
    mode: Annotated[
        ToolMode,
        "Operation mode: 'get_request_schema' returns the OpenAPI spec "
        "for the request body, 'execute' performs the actual operation",
    ],
    include_optional_properties: Annotated[
        list[str] | None,
        "Comma-separated list of optional properties to include in the response for a more comprehensive goal record.  Only used when mode is 'execute'.",  # noqa: E501
    ] = None,
    enable_pretty_output: Annotated[
        bool | None,
        "Enables pretty formatting for the API response, useful for debugging. Increases response time and size.  Only used when mode is 'execute'.",  # noqa: E501
    ] = None,
    request_body: Annotated[
        str | None,
        "Stringified JSON representing the request body. Required when "
        "mode is 'execute', ignored when mode is 'get_request_schema'",
    ] = None,
) -> Annotated[dict[str, Any], "Response from the API endpoint 'createGoal'."]:
    """Create a new goal in Asana workspace or team.

    This tool is used to create a new goal in an Asana workspace or team. It returns the full record of the newly created goal, making it essential for managing and tracking team objectives within Asana.

    Note: Understanding the request schema is necessary to properly create
    the stringified JSON input object for execution.\n\nThis operation also requires query parameters.

    Modes:
    - GET_REQUEST_SCHEMA: Returns the schema. Only call if you don't
      already have it. Do NOT call repeatedly if you already received
      the schema.
    - EXECUTE: Performs the operation with the provided request body
      JSON.\n      Note: You must also provide the required query parameters when executing.

    If you need the schema, call with mode='get_request_schema' ONCE, then execute.
    """  # noqa: E501
    if mode == ToolMode.GET_REQUEST_SCHEMA:
        return {
            "request_body_schema": REQUEST_BODY_SCHEMAS["CREATEASANAGOAL"],
            "instructions": (
                "Use the request_body_schema to construct a valid JSON object. "
                "Once you have populated the object following the schema "
                "structure and requirements, call this tool again with "
                "mode='execute' and the stringified JSON as the "
                "request_body parameter along with the required query parameters. "
                "Do NOT call the schema mode again - you already have "
                "the schema now."
            ),
        }

    # Mode is EXECUTE - validate parameters

    # Validate request body is provided (not None or empty string)
    # Note: Empty objects like {} are allowed - schema validation will check if valid
    if request_body is None or request_body.strip() == "":
        raise RetryableToolError(
            message="Request body is required when mode is 'execute'",
            developer_message="The request_body parameter was null or empty string",
            additional_prompt_content=(
                "The request body is required to perform this operation. "
                "Use the schema below to construct a valid JSON object, "
                "then call this tool again in execute mode with the "
                "stringified JSON as the request_body parameter.\n\n"
                "Schema:\n\n```json\n" + REQUEST_BODY_SCHEMAS["CREATEASANAGOAL"] + "\n```"
            ),
        )

    # Parse JSON
    try:
        request_data = json.loads(request_body)
    except json.JSONDecodeError as e:
        raise RetryableToolError(
            message=f"Invalid JSON in request body: {e!s}",
            developer_message=f"JSON parsing failed: {e!s}",
            additional_prompt_content=(
                f"The request body contains invalid JSON. Error: {e!s}\n\n"
                "Please provide a valid JSON string that matches the schema "
                "below, then call this tool again in execute mode.\n\n"
                "Schema:\n\n```json\n" + REQUEST_BODY_SCHEMAS["CREATEASANAGOAL"] + "\n```"
            ),
        ) from e

    response = await make_request_with_schema_validation(
        url="https://app.asana.com/api/1.0/goals",
        method="POST",
        request_data=request_data,
        schema=REQUEST_BODY_SCHEMAS["CREATEASANAGOAL"],
        params=remove_none_values({
            "opt_pretty": enable_pretty_output,
            "opt_fields": include_optional_properties,
        }),
        headers=remove_none_values({
            "Content-Type": "application/json",
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            ),
        }),
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(requires_auth=OAuth2(id="arcade-asana"))
async def create_goal_metric(
    context: ToolContext,
    mode: Annotated[
        ToolMode,
        "Operation mode: 'get_request_schema' returns the OpenAPI spec "
        "for the request body, 'execute' performs the actual operation",
    ],
    goal_global_id: Annotated[
        str | None,
        "Globally unique identifier for the goal to which the metric will be added.  Required when mode is 'execute', ignored when mode is 'get_request_schema'.",  # noqa: E501
    ] = None,
    include_optional_properties: Annotated[
        list[str] | None,
        "Specify the optional properties to include in the response as a list of strings.  Only used when mode is 'execute'.",  # noqa: E501
    ] = None,
    enable_pretty_output: Annotated[
        bool | None,
        "Set to true for formatted, human-readable output in JSON. Ideal for debugging. May increase response time and size.  Only used when mode is 'execute'.",  # noqa: E501
    ] = None,
    request_body: Annotated[
        str | None,
        "Stringified JSON representing the request body. Required when "
        "mode is 'execute', ignored when mode is 'get_request_schema'",
    ] = None,
) -> Annotated[dict[str, Any], "Response from the API endpoint 'createGoalMetric'."]:
    """Create and add a goal metric to a specific goal.

    Use this tool to create and add a metric to a specified goal in Asana. It replaces an existing goal metric if one already exists.

    Note: Understanding the request schema is necessary to properly create
    the stringified JSON input object for execution.\n\nThis operation also requires path, query parameters.

    Modes:
    - GET_REQUEST_SCHEMA: Returns the schema. Only call if you don't
      already have it. Do NOT call repeatedly if you already received
      the schema.
    - EXECUTE: Performs the operation with the provided request body
      JSON.\n      Note: You must also provide the required path, query parameters when executing.

    If you need the schema, call with mode='get_request_schema' ONCE, then execute.
    """  # noqa: E501
    if mode == ToolMode.GET_REQUEST_SCHEMA:
        return {
            "request_body_schema": REQUEST_BODY_SCHEMAS["CREATEGOALMETRIC"],
            "instructions": (
                "Use the request_body_schema to construct a valid JSON object. "
                "Once you have populated the object following the schema "
                "structure and requirements, call this tool again with "
                "mode='execute' and the stringified JSON as the "
                "request_body parameter along with the required path, query parameters. "
                "Do NOT call the schema mode again - you already have "
                "the schema now."
            ),
        }

    # Mode is EXECUTE - validate parameters
    # Validate required parameters
    missing_params = []
    if not goal_global_id:
        missing_params.append(("goal_global_id", "path"))

    if missing_params:
        param_names = [p[0] for p in missing_params]
        param_details = ", ".join([f"{p[0]} ({p[1]})" for p in missing_params])
        raise RetryableToolError(
            message=f"Missing required parameters: {param_names}",
            developer_message=(f"Required parameters validation failed: {param_details}"),
            additional_prompt_content=(
                f"The following required parameters are missing: "
                f"{param_details}. Please call this tool again with all "
                "required parameters."
            ),
        )

    # Validate request body is provided (not None or empty string)
    # Note: Empty objects like {} are allowed - schema validation will check if valid
    if request_body is None or request_body.strip() == "":
        raise RetryableToolError(
            message="Request body is required when mode is 'execute'",
            developer_message="The request_body parameter was null or empty string",
            additional_prompt_content=(
                "The request body is required to perform this operation. "
                "Use the schema below to construct a valid JSON object, "
                "then call this tool again in execute mode with the "
                "stringified JSON as the request_body parameter.\n\n"
                "Schema:\n\n```json\n" + REQUEST_BODY_SCHEMAS["CREATEGOALMETRIC"] + "\n```"
            ),
        )

    # Parse JSON
    try:
        request_data = json.loads(request_body)
    except json.JSONDecodeError as e:
        raise RetryableToolError(
            message=f"Invalid JSON in request body: {e!s}",
            developer_message=f"JSON parsing failed: {e!s}",
            additional_prompt_content=(
                f"The request body contains invalid JSON. Error: {e!s}\n\n"
                "Please provide a valid JSON string that matches the schema "
                "below, then call this tool again in execute mode.\n\n"
                "Schema:\n\n```json\n" + REQUEST_BODY_SCHEMAS["CREATEGOALMETRIC"] + "\n```"
            ),
        ) from e

    response = await make_request_with_schema_validation(
        url="https://app.asana.com/api/1.0/goals/{goal_gid}/setMetric".format(  # noqa: UP032
            goal_gid=goal_global_id
        ),
        method="POST",
        request_data=request_data,
        schema=REQUEST_BODY_SCHEMAS["CREATEGOALMETRIC"],
        params=remove_none_values({
            "opt_pretty": enable_pretty_output,
            "opt_fields": include_optional_properties,
        }),
        headers=remove_none_values({
            "Content-Type": "application/json",
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            ),
        }),
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(requires_auth=OAuth2(id="arcade-asana"))
async def update_goal_metric(
    context: ToolContext,
    goal_unique_identifier: Annotated[
        str, "Globally unique identifier for the Asana goal to be updated."
    ],
    current_metric_value: Annotated[
        float | None,
        "The current numeric value of a goal metric. Required if metric type is number.",
    ] = None,
    include_optional_fields: Annotated[
        list[str] | None, "Comma-separated list of optional properties to include in the response."
    ] = None,
    pretty_output: Annotated[
        bool | None, "Set to true for a more readable response format, useful for debugging."
    ] = None,
    resource_base_type: Annotated[
        str | None,
        "The base type of the resource to update. It must be a string indicating the type of resource.",  # noqa: E501
    ] = None,
    resource_unique_identifier: Annotated[
        str | None, "Globally unique identifier of the resource in Asana, represented as a string."
    ] = None,
) -> Annotated[dict[str, Any], "Response from the API endpoint 'updateGoalMetric'."]:
    """Updates a goal's current metric value in Asana.

    Use this tool to update the existing metric value of a goal in Asana. It returns the complete updated goal metric record. If the metric does not exist, it will respond with an error."""  # noqa: E501
    request_data: Any = {
        "data": {
            "gid": resource_unique_identifier,
            "resource_type": resource_base_type,
            "current_number_value": current_metric_value,
        }
    }
    if isinstance(request_data, dict):
        request_data = remove_none_values(request_data)
    content = json.dumps(request_data) if request_data else None
    response = await make_request(
        url="https://app.asana.com/api/1.0/goals/{goal_gid}/setMetricCurrentValue".format(  # noqa: UP032
            goal_gid=goal_unique_identifier
        ),
        method="POST",
        params=remove_none_values({
            "opt_pretty": pretty_output,
            "opt_fields": include_optional_fields,
        }),
        headers=remove_none_values({
            "Content-Type": "application/json",
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            ),
        }),
        content=content,
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(requires_auth=OAuth2(id="arcade-asana"))
async def add_followers_to_goal(
    context: ToolContext,
    goal_unique_id: Annotated[
        str, "Globally unique identifier for the goal to which you want to add followers."
    ],
    enable_pretty_output: Annotated[
        bool | None,
        "Set to true to receive the response in a human-readable format with proper indentation and line breaks. Useful for debugging but increases response size and time.",  # noqa: E501
    ] = None,
    optional_properties_to_include: Annotated[
        list[str] | None,
        "Comma-separated list of optional properties to include in the response for additional details.",  # noqa: E501
    ] = None,
    user_identifiers_array: Annotated[
        list[str] | None,
        "An array of user identifiers to add as followers. These can be 'me', emails, or user gids.",  # noqa: E501
    ] = None,
) -> Annotated[dict[str, Any], "Response from the API endpoint 'addFollowers'."]:
    """Add followers to a specific goal in Asana.

    Use this tool to add followers to a specific goal in Asana. It returns the updated goal details after successfully adding the followers."""  # noqa: E501
    request_data: Any = {"data": {"followers": user_identifiers_array}}
    if isinstance(request_data, dict):
        request_data = remove_none_values(request_data)
    content = json.dumps(request_data) if request_data else None
    response = await make_request(
        url="https://app.asana.com/api/1.0/goals/{goal_gid}/addFollowers".format(  # noqa: UP032
            goal_gid=goal_unique_id
        ),
        method="POST",
        params=remove_none_values({
            "opt_pretty": enable_pretty_output,
            "opt_fields": optional_properties_to_include,
        }),
        headers=remove_none_values({
            "Content-Type": "application/json",
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            ),
        }),
        content=content,
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(requires_auth=OAuth2(id="arcade-asana"))
async def remove_goal_followers(
    context: ToolContext,
    goal_unique_id: Annotated[
        str, "Globally unique identifier for the goal to remove followers from."
    ],
    enable_pretty_output: Annotated[
        bool | None,
        "Enable pretty JSON output with line breaking and indentation. Useful for debugging; increases response time and size.",  # noqa: E501
    ] = None,
    followers_to_remove: Annotated[
        list[str] | None,
        'An array of user identifiers to remove as followers. These can be "me", an email, or a user gid.',  # noqa: E501
    ] = None,
    included_optional_fields: Annotated[
        list[str] | None,
        "Comma-separated list of optional properties to include in the response for additional details.",  # noqa: E501
    ] = None,
) -> Annotated[dict[str, Any], "Response from the API endpoint 'removeFollowers'."]:
    """Remove followers from a specific goal in Asana.

    Use this tool to remove followers from a goal in Asana. It returns the complete updated goal record with the followers removed. Ideal for managing team members involved with specific goals."""  # noqa: E501
    request_data: Any = {"data": {"followers": followers_to_remove}}
    if isinstance(request_data, dict):
        request_data = remove_none_values(request_data)
    content = json.dumps(request_data) if request_data else None
    response = await make_request(
        url="https://app.asana.com/api/1.0/goals/{goal_gid}/removeFollowers".format(  # noqa: UP032
            goal_gid=goal_unique_id
        ),
        method="POST",
        params=remove_none_values({
            "opt_pretty": enable_pretty_output,
            "opt_fields": included_optional_fields,
        }),
        headers=remove_none_values({
            "Content-Type": "application/json",
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            ),
        }),
        content=content,
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(requires_auth=OAuth2(id="arcade-asana", scopes=["goals:read"]))
async def get_parent_goals_for_goal(
    context: ToolContext,
    goal_unique_identifier: Annotated[
        str, "Globally unique identifier for the Asana goal whose parent goals are to be fetched."
    ],
    include_optional_properties: Annotated[
        list[str] | None,
        "List of optional properties to include in the response. It should be a comma-separated list of strings.",  # noqa: E501
    ] = None,
    pretty_output_enabled: Annotated[
        bool | None,
        "Set to true for a readable output format with line breaks and indentation, suitable for debugging. May increase response size and time.",  # noqa: E501
    ] = None,
) -> Annotated[dict[str, Any], "Response from the API endpoint 'getParentGoalsForGoal'."]:
    """Fetches parent goals for a specific Asana goal.

    Use this tool to obtain a list of parent goals for a specified goal in Asana. It requires the 'goals:read' scope to access the data."""  # noqa: E501
    request_data: Any = None
    if isinstance(request_data, dict):
        request_data = remove_none_values(request_data)
    content = json.dumps(request_data) if request_data else None
    response = await make_request(
        url="https://app.asana.com/api/1.0/goals/{goal_gid}/parentGoals".format(  # noqa: UP032
            goal_gid=goal_unique_identifier
        ),
        method="GET",
        params=remove_none_values({
            "opt_pretty": pretty_output_enabled,
            "opt_fields": include_optional_properties,
        }),
        headers=remove_none_values({
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            )
        }),
        content=content,
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(requires_auth=OAuth2(id="arcade-asana"))
async def fetch_job_details(
    context: ToolContext,
    job_id: Annotated[str, "Globally unique identifier for the job to fetch details for."],
    include_optional_properties: Annotated[
        list[str] | None,
        "Comma-separated list of optional properties to include in the response for the job.",
    ] = None,
    opt_pretty: Annotated[
        bool | None,
        "Set to true for pretty output with line breaks and indentation. Use this for debugging as it increases response time and size.",  # noqa: E501
    ] = None,
) -> Annotated[dict[str, Any], "Response from the API endpoint 'getJob'."]:
    """Fetch complete details for a specific job record in Asana.

    Use this tool to obtain the full record of a job by providing the job ID. Ideal for retrieving detailed information about specific jobs in Asana."""  # noqa: E501
    request_data: Any = None
    if isinstance(request_data, dict):
        request_data = remove_none_values(request_data)
    content = json.dumps(request_data) if request_data else None
    response = await make_request(
        url="https://app.asana.com/api/1.0/jobs/{job_gid}".format(job_gid=job_id),  # noqa: UP032
        method="GET",
        params=remove_none_values({
            "opt_pretty": opt_pretty,
            "opt_fields": include_optional_properties,
        }),
        headers=remove_none_values({
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            )
        }),
        content=content,
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(requires_auth=OAuth2(id="arcade-asana"))
async def get_memberships(
    context: ToolContext,
    enable_pretty_output: Annotated[
        bool | None,
        "Enable pretty JSON output with line breaks and indentation for readability. Use for debugging purposes as it may increase response time and size.",  # noqa: E501
    ] = None,
    include_optional_fields: Annotated[
        list[str] | None, "Comma-separated list of optional fields to include in the response."
    ] = None,
    member_identifier: Annotated[
        str | None, "Globally unique identifier for a team or user to filter specific memberships."
    ] = None,
    pagination_offset_token: Annotated[
        str | None,
        "Token for pagination. Use the offset from a previous API response to get the next page of results.",  # noqa: E501
    ] = None,
    parent_id: Annotated[
        str | None,
        "Globally unique identifier for a goal, project, portfolio, or custom field in Asana.",
    ] = None,
    results_per_page: Annotated[
        int | None, "Specifies the number of objects to return per page. Must be between 1 and 100."
    ] = None,
) -> Annotated[dict[str, Any], "Response from the API endpoint 'getMemberships'."]:
    """Retrieve compact membership records from Asana.

    This tool fetches compact membership records such as goal, project, portfolio, or custom field memberships from Asana. It can filter results to specific memberships by using a member ID."""  # noqa: E501
    request_data: Any = None
    if isinstance(request_data, dict):
        request_data = remove_none_values(request_data)
    content = json.dumps(request_data) if request_data else None
    response = await make_request(
        url="https://app.asana.com/api/1.0/memberships",
        method="GET",
        params=remove_none_values({
            "opt_pretty": enable_pretty_output,
            "parent": parent_id,
            "member": member_identifier,
            "limit": results_per_page,
            "offset": pagination_offset_token,
            "opt_fields": include_optional_fields,
        }),
        headers=remove_none_values({
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            )
        }),
        content=content,
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(requires_auth=OAuth2(id="arcade-asana"))
async def create_asana_membership(
    context: ToolContext,
    mode: Annotated[
        ToolMode,
        "Operation mode: 'get_request_schema' returns the OpenAPI spec "
        "for the request body, 'execute' performs the actual operation",
    ],
    enable_pretty_output: Annotated[
        bool | None,
        "If true, returns the response in a formatted JSON with line breaks and indentation. Recommended only for debugging, as it increases response size and processing time.  Only used when mode is 'execute'.",  # noqa: E501
    ] = None,
    request_body: Annotated[
        str | None,
        "Stringified JSON representing the request body. Required when "
        "mode is 'execute', ignored when mode is 'get_request_schema'",
    ] = None,
) -> Annotated[dict[str, Any], "Response from the API endpoint 'createMembership'."]:
    """Create a new membership in Asana for goals, projects, portfolios, or custom fields.

    This tool creates a new membership in a specified Asana entity, such as a goal, project, portfolio, or custom field. It is used to add teams or users as members and returns the complete record of the newly created membership.

    Note: Understanding the request schema is necessary to properly create
    the stringified JSON input object for execution.\n\nThis operation also requires query parameters.

    Modes:
    - GET_REQUEST_SCHEMA: Returns the schema. Only call if you don't
      already have it. Do NOT call repeatedly if you already received
      the schema.
    - EXECUTE: Performs the operation with the provided request body
      JSON.\n      Note: You must also provide the required query parameters when executing.

    If you need the schema, call with mode='get_request_schema' ONCE, then execute.
    """  # noqa: E501
    if mode == ToolMode.GET_REQUEST_SCHEMA:
        return {
            "request_body_schema": REQUEST_BODY_SCHEMAS["CREATEASANAMEMBERSHIP"],
            "instructions": (
                "Use the request_body_schema to construct a valid JSON object. "
                "Once you have populated the object following the schema "
                "structure and requirements, call this tool again with "
                "mode='execute' and the stringified JSON as the "
                "request_body parameter along with the required query parameters. "
                "Do NOT call the schema mode again - you already have "
                "the schema now."
            ),
        }

    # Mode is EXECUTE - validate parameters

    # Validate request body is provided (not None or empty string)
    # Note: Empty objects like {} are allowed - schema validation will check if valid
    if request_body is None or request_body.strip() == "":
        raise RetryableToolError(
            message="Request body is required when mode is 'execute'",
            developer_message="The request_body parameter was null or empty string",
            additional_prompt_content=(
                "The request body is required to perform this operation. "
                "Use the schema below to construct a valid JSON object, "
                "then call this tool again in execute mode with the "
                "stringified JSON as the request_body parameter.\n\n"
                "Schema:\n\n```json\n" + REQUEST_BODY_SCHEMAS["CREATEASANAMEMBERSHIP"] + "\n```"
            ),
        )

    # Parse JSON
    try:
        request_data = json.loads(request_body)
    except json.JSONDecodeError as e:
        raise RetryableToolError(
            message=f"Invalid JSON in request body: {e!s}",
            developer_message=f"JSON parsing failed: {e!s}",
            additional_prompt_content=(
                f"The request body contains invalid JSON. Error: {e!s}\n\n"
                "Please provide a valid JSON string that matches the schema "
                "below, then call this tool again in execute mode.\n\n"
                "Schema:\n\n```json\n" + REQUEST_BODY_SCHEMAS["CREATEASANAMEMBERSHIP"] + "\n```"
            ),
        ) from e

    response = await make_request_with_schema_validation(
        url="https://app.asana.com/api/1.0/memberships",
        method="POST",
        request_data=request_data,
        schema=REQUEST_BODY_SCHEMAS["CREATEASANAMEMBERSHIP"],
        params=remove_none_values({"opt_pretty": enable_pretty_output}),
        headers=remove_none_values({
            "Content-Type": "application/json",
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            ),
        }),
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(requires_auth=OAuth2(id="arcade-asana"))
async def get_membership_info(
    context: ToolContext,
    membership_id: Annotated[
        str,
        "Globally unique identifier for the membership. Used to specify which membership record to retrieve.",  # noqa: E501
    ],
    enable_pretty_output: Annotated[
        bool | None,
        "Enable pretty output with indentation and line breaks for readability. Use for debugging as it increases response size and time.",  # noqa: E501
    ] = None,
) -> Annotated[dict[str, Any], "Response from the API endpoint 'getMembership'."]:
    """Retrieve membership record details by ID.

    Fetches information for a specified membership ID, including project, goal, portfolio, or custom field memberships."""  # noqa: E501
    request_data: Any = None
    if isinstance(request_data, dict):
        request_data = remove_none_values(request_data)
    content = json.dumps(request_data) if request_data else None
    response = await make_request(
        url="https://app.asana.com/api/1.0/memberships/{membership_gid}".format(  # noqa: UP032
            membership_gid=membership_id
        ),
        method="GET",
        params=remove_none_values({"opt_pretty": enable_pretty_output}),
        headers=remove_none_values({
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            )
        }),
        content=content,
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(requires_auth=OAuth2(id="arcade-asana"))
async def update_membership(
    context: ToolContext,
    membership_id: Annotated[str, "Globally unique identifier for the membership to update."],
    enable_pretty_output: Annotated[
        bool | None,
        "If true, formats the response for readability with line breaks and indentation, useful for debugging.",  # noqa: E501
    ] = None,
    member_access_level: Annotated[
        str | None,
        "Specify the access level for the member. Valid options depend on the membership type: Goals ('viewer', 'commenter', 'editor', 'admin'), Projects ('admin', 'editor', 'commenter'), Portfolios ('admin', 'editor', 'viewer'), Custom Fields ('admin', 'editor', 'user').",  # noqa: E501
    ] = None,
) -> Annotated[dict[str, Any], "Response from the API endpoint 'updateMembership'."]:
    """Update an existing membership in Asana.

    Use this tool to update fields of an existing membership for goals, projects, portfolios, or custom fields in Asana. Only the provided fields in the data block will be updated, while others remain unchanged."""  # noqa: E501
    request_data: Any = {"data": {"access_level": member_access_level}}
    if isinstance(request_data, dict):
        request_data = remove_none_values(request_data)
    content = json.dumps(request_data) if request_data else None
    response = await make_request(
        url="https://app.asana.com/api/1.0/memberships/{membership_gid}".format(  # noqa: UP032
            membership_gid=membership_id
        ),
        method="PUT",
        params=remove_none_values({"opt_pretty": enable_pretty_output}),
        headers=remove_none_values({
            "Content-Type": "application/json",
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            ),
        }),
        content=content,
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(requires_auth=OAuth2(id="arcade-asana"))
async def delete_asana_membership(
    context: ToolContext,
    membership_id: Annotated[str, "Globally unique identifier for the membership to be deleted."],
    pretty_output_enabled: Annotated[
        bool | None,
        "Enable pretty-printed JSON format for a more readable response. Recommended for debugging, as it increases response time and size.",  # noqa: E501
    ] = None,
) -> Annotated[dict[str, Any], "Response from the API endpoint 'deleteMembership'."]:
    """Delete a membership in Asana.

    Delete a specific, existing membership for a goal, project, portfolio, or custom field in Asana by providing the membership ID."""  # noqa: E501
    request_data: Any = None
    if isinstance(request_data, dict):
        request_data = remove_none_values(request_data)
    content = json.dumps(request_data) if request_data else None
    response = await make_request(
        url="https://app.asana.com/api/1.0/memberships/{membership_gid}".format(  # noqa: UP032
            membership_gid=membership_id
        ),
        method="DELETE",
        params=remove_none_values({"opt_pretty": pretty_output_enabled}),
        headers=remove_none_values({
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            )
        }),
        content=content,
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(requires_auth=OAuth2(id="arcade-asana"))
async def request_organization_export(
    context: ToolContext,
    enable_pretty_output: Annotated[
        bool | None,
        "Enable to receive the response in a formatted JSON with proper indentation. Increases response size and time; use primarily for debugging.",  # noqa: E501
    ] = None,
    include_optional_properties: Annotated[
        list[str] | None,
        "List of property names to include in the export, separated by commas. Use to include non-default properties.",  # noqa: E501
    ] = None,
    organization_id: Annotated[
        str | None,
        "Globally unique identifier for the workspace or organization to be exported in Asana.",
    ] = None,
) -> Annotated[dict[str, Any], "Response from the API endpoint 'createOrganizationExport'."]:
    """Submit a request to export an organization in Asana.

    Use this tool to initiate an organization export request in Asana. The export will be processed by Asana after the request is submitted."""  # noqa: E501
    request_data: Any = {"data": {"organization": organization_id}}
    if isinstance(request_data, dict):
        request_data = remove_none_values(request_data)
    content = json.dumps(request_data) if request_data else None
    response = await make_request(
        url="https://app.asana.com/api/1.0/organization_exports",
        method="POST",
        params=remove_none_values({
            "opt_pretty": enable_pretty_output,
            "opt_fields": include_optional_properties,
        }),
        headers=remove_none_values({
            "Content-Type": "application/json",
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            ),
        }),
        content=content,
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(requires_auth=OAuth2(id="arcade-asana"))
async def get_organization_export_details(
    context: ToolContext,
    organization_export_id: Annotated[
        str,
        "Globally unique identifier for the organization export. Required to retrieve the export details.",  # noqa: E501
    ],
    enable_pretty_output: Annotated[
        bool | None,
        "Set to true for a human-readable response with line breaks and indentation. Recommended for debugging due to increased response size.",  # noqa: E501
    ] = None,
    include_optional_properties: Annotated[
        list[str] | None,
        "Optional properties to include in the response, as a list of strings. This specifies which additional fields should be returned with the organization export details.",  # noqa: E501
    ] = None,
) -> Annotated[dict[str, Any], "Response from the API endpoint 'getOrganizationExport'."]:
    """Fetch details of a specific organization export.

    Use this tool to obtain details about a previously-requested export of an organization's data in Asana. It requires the export ID to retrieve the relevant information."""  # noqa: E501
    request_data: Any = None
    if isinstance(request_data, dict):
        request_data = remove_none_values(request_data)
    content = json.dumps(request_data) if request_data else None
    response = await make_request(
        url="https://app.asana.com/api/1.0/organization_exports/{organization_export_gid}".format(  # noqa: UP032
            organization_export_gid=organization_export_id
        ),
        method="GET",
        params=remove_none_values({
            "opt_pretty": enable_pretty_output,
            "opt_fields": include_optional_properties,
        }),
        headers=remove_none_values({
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            )
        }),
        content=content,
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(requires_auth=OAuth2(id="arcade-asana"))
async def get_portfolio_memberships(
    context: ToolContext,
    enable_pretty_output: Annotated[
        bool | None,
        "Enable pretty output formatting with indentation for readability. Ideal for debugging, it increases response time and size.",  # noqa: E501
    ] = None,
    include_optional_fields: Annotated[
        list[str] | None,
        "A list of optional properties to include in the response. Provide them as an array of strings.",  # noqa: E501
    ] = None,
    pagination_offset_token: Annotated[
        str | None,
        "Token for pagination to retrieve the next page of results. Use the offset provided by a previous response.",  # noqa: E501
    ] = None,
    portfolio_filter: Annotated[
        str | None, "Specify the portfolio ID to filter the results."
    ] = None,
    results_per_page: Annotated[
        int | None,
        "Specifies the number of portfolio memberships to return per page, between 1 and 100.",
    ] = None,
    user_identifier: Annotated[
        str | None, 'A string identifier for a user: "me", an email, or the user\'s gid.'
    ] = None,
    workspace_filter: Annotated[
        str | None,
        "Specify the workspace to filter portfolio membership results. This should match the workspace identifier used in Asana.",  # noqa: E501
    ] = None,
) -> Annotated[dict[str, Any], "Response from the API endpoint 'getPortfolioMemberships'."]:
    """Retrieve portfolio memberships from Asana.

    Retrieve a list of portfolio memberships based on specified criteria such as portfolio, user, or workspace from Asana. Useful for managing and accessing user roles in project management."""  # noqa: E501
    request_data: Any = None
    if isinstance(request_data, dict):
        request_data = remove_none_values(request_data)
    content = json.dumps(request_data) if request_data else None
    response = await make_request(
        url="https://app.asana.com/api/1.0/portfolio_memberships",
        method="GET",
        params=remove_none_values({
            "portfolio": portfolio_filter,
            "workspace": workspace_filter,
            "user": user_identifier,
            "opt_pretty": enable_pretty_output,
            "limit": results_per_page,
            "offset": pagination_offset_token,
            "opt_fields": include_optional_fields,
        }),
        headers=remove_none_values({
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            )
        }),
        content=content,
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(requires_auth=OAuth2(id="arcade-asana"))
async def get_portfolio_membership(
    context: ToolContext,
    portfolio_membership_id: Annotated[
        str, "The unique identifier for the portfolio membership to retrieve."
    ],
    enable_pretty_output: Annotated[
        bool | None,
        "If true, enables pretty JSON output with line breaking and indentation, increasing response size and time. Useful for debugging.",  # noqa: E501
    ] = None,
    include_optional_properties: Annotated[
        list[str] | None,
        "A list of optional properties to include in the response, specified as strings.",
    ] = None,
) -> Annotated[dict[str, Any], "Response from the API endpoint 'getPortfolioMembership'."]:
    """Retrieve a single portfolio membership record.

    Use this tool to obtain the complete portfolio record for a specific portfolio membership by its unique identifier."""  # noqa: E501
    request_data: Any = None
    if isinstance(request_data, dict):
        request_data = remove_none_values(request_data)
    content = json.dumps(request_data) if request_data else None
    response = await make_request(
        url="https://app.asana.com/api/1.0/portfolio_memberships/{portfolio_membership_gid}".format(  # noqa: UP032
            portfolio_membership_gid=portfolio_membership_id
        ),
        method="GET",
        params=remove_none_values({
            "opt_pretty": enable_pretty_output,
            "opt_fields": include_optional_properties,
        }),
        headers=remove_none_values({
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            )
        }),
        content=content,
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(requires_auth=OAuth2(id="arcade-asana"))
async def retrieve_portfolio_memberships(
    context: ToolContext,
    portfolio_identifier: Annotated[
        str, "Globally unique identifier for the portfolio required to fetch membership records."
    ],
    enable_pretty_output: Annotated[
        bool | None,
        "Set to true to receive a readable, formatted response. Use this primarily for debugging due to increased response size and time.",  # noqa: E501
    ] = None,
    include_optional_fields: Annotated[
        list[str] | None,
        "Specify a list of optional properties to include, using a comma-separated list.",
    ] = None,
    pagination_offset_token: Annotated[
        str | None,
        "Offset token used for pagination to retrieve the next page of results. Use the token returned from a previous request.",  # noqa: E501
    ] = None,
    results_per_page: Annotated[
        int | None, "Number of portfolio memberships to return per page, between 1 and 100."
    ] = None,
    user_identifier: Annotated[
        str | None, "A string identifying a user. This can be 'me', an email, or the user's gid."
    ] = None,
) -> Annotated[
    dict[str, Any], "Response from the API endpoint 'getPortfolioMembershipsForPortfolio'."
]:
    """Retrieve compact portfolio membership records for a portfolio.

    Call this tool to obtain a list of portfolio membership records associated with a specific portfolio in Asana. Use it when you need to view or manage the members of a portfolio."""  # noqa: E501
    request_data: Any = None
    if isinstance(request_data, dict):
        request_data = remove_none_values(request_data)
    content = json.dumps(request_data) if request_data else None
    response = await make_request(
        url="https://app.asana.com/api/1.0/portfolios/{portfolio_gid}/portfolio_memberships".format(  # noqa: UP032
            portfolio_gid=portfolio_identifier
        ),
        method="GET",
        params=remove_none_values({
            "user": user_identifier,
            "opt_pretty": enable_pretty_output,
            "limit": results_per_page,
            "offset": pagination_offset_token,
            "opt_fields": include_optional_fields,
        }),
        headers=remove_none_values({
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            )
        }),
        content=content,
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(requires_auth=OAuth2(id="arcade-asana", scopes=["portfolios:read"]))
async def get_user_owned_portfolios(
    context: ToolContext,
    workspace_identifier: Annotated[
        str, "The unique identifier for the workspace or organization to filter portfolios on."
    ],
    enable_pretty_output: Annotated[
        bool | None,
        "Enable pretty formatting for the output, improving readability with line breaks and indentation. This may increase response size and time, suitable for debugging.",  # noqa: E501
    ] = None,
    include_optional_fields: Annotated[
        list[str] | None,
        "Comma-separated list of properties to include in the response, allowing retrieval of additional optional fields.",  # noqa: E501
    ] = None,
    pagination_offset_token: Annotated[
        str | None,
        "A token to retrieve the next page of results. Use the offset token from a previous API response for pagination. If omitted, returns the first page.",  # noqa: E501
    ] = None,
    portfolio_owner_identifier: Annotated[
        str | None,
        "Specify the user who owns the portfolio. Only applicable if using a Service Account for accessing portfolios owned by different users.",  # noqa: E501
    ] = None,
    results_per_page: Annotated[
        int | None, "The number of portfolios to return per page. Must be between 1 and 100."
    ] = None,
) -> Annotated[dict[str, Any], "Response from the API endpoint 'getPortfolios'."]:
    """Retrieve a list of portfolios owned by the user.

    Use this tool to get a list of portfolios owned by the current API user in Asana. This is useful for managing and organizing projects within the user's account."""  # noqa: E501
    request_data: Any = None
    if isinstance(request_data, dict):
        request_data = remove_none_values(request_data)
    content = json.dumps(request_data) if request_data else None
    response = await make_request(
        url="https://app.asana.com/api/1.0/portfolios",
        method="GET",
        params=remove_none_values({
            "opt_pretty": enable_pretty_output,
            "limit": results_per_page,
            "offset": pagination_offset_token,
            "workspace": workspace_identifier,
            "owner": portfolio_owner_identifier,
            "opt_fields": include_optional_fields,
        }),
        headers=remove_none_values({
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            )
        }),
        content=content,
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(requires_auth=OAuth2(id="arcade-asana", scopes=["portfolios:write"]))
async def create_asana_portfolio(
    context: ToolContext,
    mode: Annotated[
        ToolMode,
        "Operation mode: 'get_request_schema' returns the OpenAPI spec "
        "for the request body, 'execute' performs the actual operation",
    ],
    include_optional_fields: Annotated[
        list[str] | None,
        "List the optional properties to include in the response, such as ['name', 'members'].  Only used when mode is 'execute'.",  # noqa: E501
    ] = None,
    enable_pretty_output: Annotated[
        bool | None,
        "If true, formats the response with line breaks and indentation for easier readability. Recommended for debugging as it may increase response size and time.  Only used when mode is 'execute'.",  # noqa: E501
    ] = None,
    request_body: Annotated[
        str | None,
        "Stringified JSON representing the request body. Required when "
        "mode is 'execute', ignored when mode is 'get_request_schema'",
    ] = None,
) -> Annotated[dict[str, Any], "Response from the API endpoint 'createPortfolio'."]:
    """Create a new portfolio in an Asana workspace.

    Use this tool to create a new portfolio in a specific Asana workspace with a supplied name. Note that this API does not automatically add states like 'Priority' that might be present in portfolios created through the Asana UI.

    Note: Understanding the request schema is necessary to properly create
    the stringified JSON input object for execution.\n\nThis operation also requires query parameters.

    Modes:
    - GET_REQUEST_SCHEMA: Returns the schema. Only call if you don't
      already have it. Do NOT call repeatedly if you already received
      the schema.
    - EXECUTE: Performs the operation with the provided request body
      JSON.\n      Note: You must also provide the required query parameters when executing.

    If you need the schema, call with mode='get_request_schema' ONCE, then execute.
    """  # noqa: E501
    if mode == ToolMode.GET_REQUEST_SCHEMA:
        return {
            "request_body_schema": REQUEST_BODY_SCHEMAS["CREATEASANAPORTFOLIO"],
            "instructions": (
                "Use the request_body_schema to construct a valid JSON object. "
                "Once you have populated the object following the schema "
                "structure and requirements, call this tool again with "
                "mode='execute' and the stringified JSON as the "
                "request_body parameter along with the required query parameters. "
                "Do NOT call the schema mode again - you already have "
                "the schema now."
            ),
        }

    # Mode is EXECUTE - validate parameters

    # Validate request body is provided (not None or empty string)
    # Note: Empty objects like {} are allowed - schema validation will check if valid
    if request_body is None or request_body.strip() == "":
        raise RetryableToolError(
            message="Request body is required when mode is 'execute'",
            developer_message="The request_body parameter was null or empty string",
            additional_prompt_content=(
                "The request body is required to perform this operation. "
                "Use the schema below to construct a valid JSON object, "
                "then call this tool again in execute mode with the "
                "stringified JSON as the request_body parameter.\n\n"
                "Schema:\n\n```json\n" + REQUEST_BODY_SCHEMAS["CREATEASANAPORTFOLIO"] + "\n```"
            ),
        )

    # Parse JSON
    try:
        request_data = json.loads(request_body)
    except json.JSONDecodeError as e:
        raise RetryableToolError(
            message=f"Invalid JSON in request body: {e!s}",
            developer_message=f"JSON parsing failed: {e!s}",
            additional_prompt_content=(
                f"The request body contains invalid JSON. Error: {e!s}\n\n"
                "Please provide a valid JSON string that matches the schema "
                "below, then call this tool again in execute mode.\n\n"
                "Schema:\n\n```json\n" + REQUEST_BODY_SCHEMAS["CREATEASANAPORTFOLIO"] + "\n```"
            ),
        ) from e

    response = await make_request_with_schema_validation(
        url="https://app.asana.com/api/1.0/portfolios",
        method="POST",
        request_data=request_data,
        schema=REQUEST_BODY_SCHEMAS["CREATEASANAPORTFOLIO"],
        params=remove_none_values({
            "opt_pretty": enable_pretty_output,
            "opt_fields": include_optional_fields,
        }),
        headers=remove_none_values({
            "Content-Type": "application/json",
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            ),
        }),
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(requires_auth=OAuth2(id="arcade-asana", scopes=["portfolios:read"]))
async def get_portfolio_details(
    context: ToolContext,
    portfolio_unique_id: Annotated[
        str, "Globally unique identifier for the specific portfolio to retrieve details."
    ],
    enable_pretty_output: Annotated[
        bool | None,
        "Set to true for pretty, readable formatting of the output. Useful for debugging, but increases response size and time.",  # noqa: E501
    ] = None,
    include_optional_properties: Annotated[
        list[str] | None,
        "List of properties to include in the response. Specify as a list of strings for fields you wish to include, as the API excludes some by default.",  # noqa: E501
    ] = None,
) -> Annotated[dict[str, Any], "Response from the API endpoint 'getPortfolio'."]:
    """Retrieve complete details of a specific portfolio in Asana.

    This tool is used to obtain the full record of a particular portfolio in Asana, given its unique ID. It requires the 'portfolios:read' scope and returns comprehensive details including any custom field settings if the 'custom_fields:read' scope is also available."""  # noqa: E501
    request_data: Any = None
    if isinstance(request_data, dict):
        request_data = remove_none_values(request_data)
    content = json.dumps(request_data) if request_data else None
    response = await make_request(
        url="https://app.asana.com/api/1.0/portfolios/{portfolio_gid}".format(  # noqa: UP032
            portfolio_gid=portfolio_unique_id
        ),
        method="GET",
        params=remove_none_values({
            "opt_pretty": enable_pretty_output,
            "opt_fields": include_optional_properties,
        }),
        headers=remove_none_values({
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            )
        }),
        content=content,
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(requires_auth=OAuth2(id="arcade-asana", scopes=["portfolios:write"]))
async def update_portfolio(
    context: ToolContext,
    mode: Annotated[
        ToolMode,
        "Operation mode: 'get_request_schema' returns the OpenAPI spec "
        "for the request body, 'execute' performs the actual operation",
    ],
    portfolio_unique_identifier: Annotated[
        str | None,
        "Globally unique identifier for the portfolio to be updated.  Required when mode is 'execute', ignored when mode is 'get_request_schema'.",  # noqa: E501
    ] = None,
    include_optional_fields: Annotated[
        list[str] | None,
        "A list of optional portfolio properties to include in the response, specified as an array of strings.  Only used when mode is 'execute'.",  # noqa: E501
    ] = None,
    enable_pretty_output: Annotated[
        bool | None,
        "Provides formatted and indented output. Use primarily for debugging as it increases response size and processing time.  Only used when mode is 'execute'.",  # noqa: E501
    ] = None,
    request_body: Annotated[
        str | None,
        "Stringified JSON representing the request body. Required when "
        "mode is 'execute', ignored when mode is 'get_request_schema'",
    ] = None,
) -> Annotated[dict[str, Any], "Response from the API endpoint 'updatePortfolio'."]:
    """Update an existing Asana portfolio.

    Use this tool to update an existing Asana portfolio. Only the specified fields in the request data will be updated, and any unspecified fields will remain unchanged. Requires the 'portfolios:write' scope.

    Note: Understanding the request schema is necessary to properly create
    the stringified JSON input object for execution.\n\nThis operation also requires path, query parameters.

    Modes:
    - GET_REQUEST_SCHEMA: Returns the schema. Only call if you don't
      already have it. Do NOT call repeatedly if you already received
      the schema.
    - EXECUTE: Performs the operation with the provided request body
      JSON.\n      Note: You must also provide the required path, query parameters when executing.

    If you need the schema, call with mode='get_request_schema' ONCE, then execute.
    """  # noqa: E501
    if mode == ToolMode.GET_REQUEST_SCHEMA:
        return {
            "request_body_schema": REQUEST_BODY_SCHEMAS["UPDATEPORTFOLIO"],
            "instructions": (
                "Use the request_body_schema to construct a valid JSON object. "
                "Once you have populated the object following the schema "
                "structure and requirements, call this tool again with "
                "mode='execute' and the stringified JSON as the "
                "request_body parameter along with the required path, query parameters. "
                "Do NOT call the schema mode again - you already have "
                "the schema now."
            ),
        }

    # Mode is EXECUTE - validate parameters
    # Validate required parameters
    missing_params = []
    if not portfolio_unique_identifier:
        missing_params.append(("portfolio_unique_identifier", "path"))

    if missing_params:
        param_names = [p[0] for p in missing_params]
        param_details = ", ".join([f"{p[0]} ({p[1]})" for p in missing_params])
        raise RetryableToolError(
            message=f"Missing required parameters: {param_names}",
            developer_message=(f"Required parameters validation failed: {param_details}"),
            additional_prompt_content=(
                f"The following required parameters are missing: "
                f"{param_details}. Please call this tool again with all "
                "required parameters."
            ),
        )

    # Validate request body is provided (not None or empty string)
    # Note: Empty objects like {} are allowed - schema validation will check if valid
    if request_body is None or request_body.strip() == "":
        raise RetryableToolError(
            message="Request body is required when mode is 'execute'",
            developer_message="The request_body parameter was null or empty string",
            additional_prompt_content=(
                "The request body is required to perform this operation. "
                "Use the schema below to construct a valid JSON object, "
                "then call this tool again in execute mode with the "
                "stringified JSON as the request_body parameter.\n\n"
                "Schema:\n\n```json\n" + REQUEST_BODY_SCHEMAS["UPDATEPORTFOLIO"] + "\n```"
            ),
        )

    # Parse JSON
    try:
        request_data = json.loads(request_body)
    except json.JSONDecodeError as e:
        raise RetryableToolError(
            message=f"Invalid JSON in request body: {e!s}",
            developer_message=f"JSON parsing failed: {e!s}",
            additional_prompt_content=(
                f"The request body contains invalid JSON. Error: {e!s}\n\n"
                "Please provide a valid JSON string that matches the schema "
                "below, then call this tool again in execute mode.\n\n"
                "Schema:\n\n```json\n" + REQUEST_BODY_SCHEMAS["UPDATEPORTFOLIO"] + "\n```"
            ),
        ) from e

    response = await make_request_with_schema_validation(
        url="https://app.asana.com/api/1.0/portfolios/{portfolio_gid}".format(  # noqa: UP032
            portfolio_gid=portfolio_unique_identifier
        ),
        method="PUT",
        request_data=request_data,
        schema=REQUEST_BODY_SCHEMAS["UPDATEPORTFOLIO"],
        params=remove_none_values({
            "opt_pretty": enable_pretty_output,
            "opt_fields": include_optional_fields,
        }),
        headers=remove_none_values({
            "Content-Type": "application/json",
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            ),
        }),
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(requires_auth=OAuth2(id="arcade-asana"))
async def delete_portfolio(
    context: ToolContext,
    portfolio_global_id: Annotated[
        str, "Globally unique identifier for the portfolio to be deleted."
    ],
    enable_pretty_output: Annotated[
        bool | None,
        "Enable this to receive a 'pretty' formatted JSON response, advisable for debugging only as it increases response time and size.",  # noqa: E501
    ] = None,
) -> Annotated[dict[str, Any], "Response from the API endpoint 'deletePortfolio'."]:
    """Delete an existing portfolio in Asana.

    Use this tool to delete an existing portfolio by specifying the portfolio's ID in Asana. It should be called when a user needs to remove a portfolio from their system."""  # noqa: E501
    request_data: Any = None
    if isinstance(request_data, dict):
        request_data = remove_none_values(request_data)
    content = json.dumps(request_data) if request_data else None
    response = await make_request(
        url="https://app.asana.com/api/1.0/portfolios/{portfolio_gid}".format(  # noqa: UP032
            portfolio_gid=portfolio_global_id
        ),
        method="DELETE",
        params=remove_none_values({"opt_pretty": enable_pretty_output}),
        headers=remove_none_values({
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            )
        }),
        content=content,
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(requires_auth=OAuth2(id="arcade-asana", scopes=["portfolios:read"]))
async def get_portfolio_items(
    context: ToolContext,
    portfolio_unique_id: Annotated[
        str,
        "Globally unique identifier for the portfolio. Required to fetch items for a specific portfolio.",  # noqa: E501
    ],
    enable_pretty_output: Annotated[
        bool | None,
        "If true, the response will be formatted for readability with line breaks and indentation. Recommended for debugging as this can increase response size and time.",  # noqa: E501
    ] = None,
    include_optional_properties: Annotated[
        list[str] | None, "Comma-separated list of optional properties to include in the response."
    ] = None,
    pagination_offset_token: Annotated[
        str | None,
        "Token for offsetting to the next page of results. Use a token from previous pagination to retrieve subsequent pages.",  # noqa: E501
    ] = None,
    results_per_page: Annotated[
        int | None, "The number of items to return per page, between 1 and 100."
    ] = None,
) -> Annotated[dict[str, Any], "Response from the API endpoint 'getItemsForPortfolio'."]:
    """Retrieve a list of items in a portfolio.

    This tool fetches a list of items in compact form within a specified portfolio. It requires the 'portfolios:read' scope to access the portfolio data."""  # noqa: E501
    request_data: Any = None
    if isinstance(request_data, dict):
        request_data = remove_none_values(request_data)
    content = json.dumps(request_data) if request_data else None
    response = await make_request(
        url="https://app.asana.com/api/1.0/portfolios/{portfolio_gid}/items".format(  # noqa: UP032
            portfolio_gid=portfolio_unique_id
        ),
        method="GET",
        params=remove_none_values({
            "opt_pretty": enable_pretty_output,
            "limit": results_per_page,
            "offset": pagination_offset_token,
            "opt_fields": include_optional_properties,
        }),
        headers=remove_none_values({
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            )
        }),
        content=content,
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(requires_auth=OAuth2(id="arcade-asana", scopes=["portfolios:write"]))
async def add_item_to_portfolio(
    context: ToolContext,
    portfolio_unique_id: Annotated[
        str, "Globally unique identifier for the portfolio to which the item will be added."
    ],
    add_after_item_id: Annotated[
        str | None,
        "ID of an item in the portfolio where the new item will be added after. Cannot be used with 'add_before_item_id'.",  # noqa: E501
    ] = None,
    enable_pretty_output: Annotated[
        bool | None,
        "Enable pretty JSON formatting for debugging. This increases response size and processing time.",  # noqa: E501
    ] = None,
    insert_item_before_id: Annotated[
        str | None,
        "ID of an existing portfolio item. The new item will be placed before this item. Cannot be used with `insert_after_id`.",  # noqa: E501
    ] = None,
    item_to_add_to_portfolio: Annotated[
        str | None, "The ID of the item to be added to the Asana portfolio."
    ] = None,
) -> Annotated[dict[str, Any], "Response from the API endpoint 'addItemForPortfolio'."]:
    """Add an item to a portfolio in Asana.

    Use this tool to add an item to a specified portfolio in Asana. Requires 'portfolios:write' permission."""  # noqa: E501
    request_data: Any = {
        "data": {
            "item": item_to_add_to_portfolio,
            "insert_before": insert_item_before_id,
            "insert_after": add_after_item_id,
        }
    }
    if isinstance(request_data, dict):
        request_data = remove_none_values(request_data)
    content = json.dumps(request_data) if request_data else None
    response = await make_request(
        url="https://app.asana.com/api/1.0/portfolios/{portfolio_gid}/addItem".format(  # noqa: UP032
            portfolio_gid=portfolio_unique_id
        ),
        method="POST",
        params=remove_none_values({"opt_pretty": enable_pretty_output}),
        headers=remove_none_values({
            "Content-Type": "application/json",
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            ),
        }),
        content=content,
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(requires_auth=OAuth2(id="arcade-asana", scopes=["portfolios:write"]))
async def remove_item_from_portfolio(
    context: ToolContext,
    portfolio_identifier: Annotated[
        str, "Globally unique identifier for the portfolio to remove the item from."
    ],
    enable_pretty_output: Annotated[
        bool | None,
        "Enable this to receive the response in a readable, formatted way. This can increase response size and is best for debugging.",  # noqa: E501
    ] = None,
    item_to_remove: Annotated[
        str | None, "Specify the ID of the item to be removed from the portfolio."
    ] = None,
) -> Annotated[dict[str, Any], "Response from the API endpoint 'removeItemForPortfolio'."]:
    """Remove an item from a portfolio in Asana.

    Use this tool to remove an item from a specified portfolio in Asana. Requires the 'portfolios:write' scope."""  # noqa: E501
    request_data: Any = {"data": {"item": item_to_remove}}
    if isinstance(request_data, dict):
        request_data = remove_none_values(request_data)
    content = json.dumps(request_data) if request_data else None
    response = await make_request(
        url="https://app.asana.com/api/1.0/portfolios/{portfolio_gid}/removeItem".format(  # noqa: UP032
            portfolio_gid=portfolio_identifier
        ),
        method="POST",
        params=remove_none_values({"opt_pretty": enable_pretty_output}),
        headers=remove_none_values({
            "Content-Type": "application/json",
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            ),
        }),
        content=content,
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(requires_auth=OAuth2(id="arcade-asana", scopes=["portfolios:write"]))
async def add_custom_field_to_portfolio(
    context: ToolContext,
    mode: Annotated[
        ToolMode,
        "Operation mode: 'get_request_schema' returns the OpenAPI spec "
        "for the request body, 'execute' performs the actual operation",
    ],
    portfolio_identifier: Annotated[
        str | None,
        "Globally unique identifier for the portfolio to which the custom field will be added.  Required when mode is 'execute', ignored when mode is 'get_request_schema'.",  # noqa: E501
    ] = None,
    pretty_formatting_enabled: Annotated[
        bool | None,
        "Set to true to format the response for better readability, useful for debugging.  Only used when mode is 'execute'.",  # noqa: E501
    ] = None,
    request_body: Annotated[
        str | None,
        "Stringified JSON representing the request body. Required when "
        "mode is 'execute', ignored when mode is 'get_request_schema'",
    ] = None,
) -> Annotated[
    dict[str, Any], "Response from the API endpoint 'addCustomFieldSettingForPortfolio'."
]:
    """Add a custom field setting to an Asana portfolio.

    Use this tool to associate a custom field with a specific portfolio in Asana. This requires portfolios:write access scope.

    Note: Understanding the request schema is necessary to properly create
    the stringified JSON input object for execution.\n\nThis operation also requires path, query parameters.

    Modes:
    - GET_REQUEST_SCHEMA: Returns the schema. Only call if you don't
      already have it. Do NOT call repeatedly if you already received
      the schema.
    - EXECUTE: Performs the operation with the provided request body
      JSON.\n      Note: You must also provide the required path, query parameters when executing.

    If you need the schema, call with mode='get_request_schema' ONCE, then execute.
    """  # noqa: E501
    if mode == ToolMode.GET_REQUEST_SCHEMA:
        return {
            "request_body_schema": REQUEST_BODY_SCHEMAS["ADDCUSTOMFIELDTOPORTFOLIO"],
            "instructions": (
                "Use the request_body_schema to construct a valid JSON object. "
                "Once you have populated the object following the schema "
                "structure and requirements, call this tool again with "
                "mode='execute' and the stringified JSON as the "
                "request_body parameter along with the required path, query parameters. "
                "Do NOT call the schema mode again - you already have "
                "the schema now."
            ),
        }

    # Mode is EXECUTE - validate parameters
    # Validate required parameters
    missing_params = []
    if not portfolio_identifier:
        missing_params.append(("portfolio_identifier", "path"))

    if missing_params:
        param_names = [p[0] for p in missing_params]
        param_details = ", ".join([f"{p[0]} ({p[1]})" for p in missing_params])
        raise RetryableToolError(
            message=f"Missing required parameters: {param_names}",
            developer_message=(f"Required parameters validation failed: {param_details}"),
            additional_prompt_content=(
                f"The following required parameters are missing: "
                f"{param_details}. Please call this tool again with all "
                "required parameters."
            ),
        )

    # Validate request body is provided (not None or empty string)
    # Note: Empty objects like {} are allowed - schema validation will check if valid
    if request_body is None or request_body.strip() == "":
        raise RetryableToolError(
            message="Request body is required when mode is 'execute'",
            developer_message="The request_body parameter was null or empty string",
            additional_prompt_content=(
                "The request body is required to perform this operation. "
                "Use the schema below to construct a valid JSON object, "
                "then call this tool again in execute mode with the "
                "stringified JSON as the request_body parameter.\n\n"
                "Schema:\n\n```json\n" + REQUEST_BODY_SCHEMAS["ADDCUSTOMFIELDTOPORTFOLIO"] + "\n```"
            ),
        )

    # Parse JSON
    try:
        request_data = json.loads(request_body)
    except json.JSONDecodeError as e:
        raise RetryableToolError(
            message=f"Invalid JSON in request body: {e!s}",
            developer_message=f"JSON parsing failed: {e!s}",
            additional_prompt_content=(
                f"The request body contains invalid JSON. Error: {e!s}\n\n"
                "Please provide a valid JSON string that matches the schema "
                "below, then call this tool again in execute mode.\n\n"
                "Schema:\n\n```json\n" + REQUEST_BODY_SCHEMAS["ADDCUSTOMFIELDTOPORTFOLIO"] + "\n```"
            ),
        ) from e

    response = await make_request_with_schema_validation(
        url="https://app.asana.com/api/1.0/portfolios/{portfolio_gid}/addCustomFieldSetting".format(  # noqa: UP032
            portfolio_gid=portfolio_identifier
        ),
        method="POST",
        request_data=request_data,
        schema=REQUEST_BODY_SCHEMAS["ADDCUSTOMFIELDTOPORTFOLIO"],
        params=remove_none_values({"opt_pretty": pretty_formatting_enabled}),
        headers=remove_none_values({
            "Content-Type": "application/json",
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            ),
        }),
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(requires_auth=OAuth2(id="arcade-asana", scopes=["portfolios:write"]))
async def remove_custom_field_from_portfolio(
    context: ToolContext,
    portfolio_global_id: Annotated[
        str,
        "Globally unique identifier for the portfolio to identify which portfolio the custom field should be removed from.",  # noqa: E501
    ],
    custom_field_id_to_remove: Annotated[
        str | None, "The unique identifier of the custom field to be removed from the portfolio."
    ] = None,
    enable_pretty_output: Annotated[
        bool | None,
        "Enable pretty format for the output, adding line breaks and indentation for readability. Increases response size and processing time, recommended for debugging.",  # noqa: E501
    ] = None,
) -> Annotated[
    dict[str, Any], "Response from the API endpoint 'removeCustomFieldSettingForPortfolio'."
]:
    """Removes a custom field from an Asana portfolio.

    Use this tool to remove a custom field setting from a specific portfolio in Asana. Useful when you need to update portfolio configurations by eliminating unnecessary custom fields."""  # noqa: E501
    request_data: Any = {"data": {"custom_field": custom_field_id_to_remove}}
    if isinstance(request_data, dict):
        request_data = remove_none_values(request_data)
    content = json.dumps(request_data) if request_data else None
    response = await make_request(
        url="https://app.asana.com/api/1.0/portfolios/{portfolio_gid}/removeCustomFieldSetting".format(  # noqa: UP032
            portfolio_gid=portfolio_global_id
        ),
        method="POST",
        params=remove_none_values({"opt_pretty": enable_pretty_output}),
        headers=remove_none_values({
            "Content-Type": "application/json",
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            ),
        }),
        content=content,
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(requires_auth=OAuth2(id="arcade-asana"))
async def add_portfolio_members(
    context: ToolContext,
    portfolio_global_id: Annotated[
        str, "Globally unique identifier for the portfolio to which members will be added."
    ],
    enable_pretty_output: Annotated[
        bool | None,
        "Set to true for readable line-breaking and indentation in the response. Use for debugging as it increases response size and time.",  # noqa: E501
    ] = None,
    include_optional_properties: Annotated[
        list[str] | None,
        "A list of optional properties to include in the response, provided as an array of strings.",  # noqa: E501
    ] = None,
    portfolio_member_identifiers: Annotated[
        str | None, "An array of strings identifying users to add. Use 'me', an email, or user gid."
    ] = None,
) -> Annotated[dict[str, Any], "Response from the API endpoint 'addMembersForPortfolio'."]:
    """Add specified users as members of a portfolio on Asana.

    Use this tool to add a list of users as members to a specific portfolio in Asana. This returns the updated portfolio record with the new members included."""  # noqa: E501
    request_data: Any = {"data": {"members": portfolio_member_identifiers}}
    if isinstance(request_data, dict):
        request_data = remove_none_values(request_data)
    content = json.dumps(request_data) if request_data else None
    response = await make_request(
        url="https://app.asana.com/api/1.0/portfolios/{portfolio_gid}/addMembers".format(  # noqa: UP032
            portfolio_gid=portfolio_global_id
        ),
        method="POST",
        params=remove_none_values({
            "opt_pretty": enable_pretty_output,
            "opt_fields": include_optional_properties,
        }),
        headers=remove_none_values({
            "Content-Type": "application/json",
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            ),
        }),
        content=content,
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(requires_auth=OAuth2(id="arcade-asana"))
async def remove_portfolio_members(
    context: ToolContext,
    portfolio_unique_id: Annotated[
        str,
        "Globally unique identifier for the portfolio to modify. Required for removing members.",
    ],
    enable_pretty_output: Annotated[
        bool | None,
        "Enable to format the response with line breaks and indentation for readability. Recommended only for debugging.",  # noqa: E501
    ] = None,
    include_optional_properties: Annotated[
        list[str] | None,
        "Comma-separated list of properties to include in the response, allowing for additional data retrieval.",  # noqa: E501
    ] = None,
    members_to_remove: Annotated[
        str | None,
        "List of user identifiers to remove from the portfolio. Use 'me', an email, or user gid.",
    ] = None,
) -> Annotated[dict[str, Any], "Response from the API endpoint 'removeMembersForPortfolio'."]:
    """Remove specified members from a portfolio.

    Use this tool to remove a list of users from the members of a specific Asana portfolio. The tool returns the updated portfolio record after the members have been removed."""  # noqa: E501
    request_data: Any = {"data": {"members": members_to_remove}}
    if isinstance(request_data, dict):
        request_data = remove_none_values(request_data)
    content = json.dumps(request_data) if request_data else None
    response = await make_request(
        url="https://app.asana.com/api/1.0/portfolios/{portfolio_gid}/removeMembers".format(  # noqa: UP032
            portfolio_gid=portfolio_unique_id
        ),
        method="POST",
        params=remove_none_values({
            "opt_pretty": enable_pretty_output,
            "opt_fields": include_optional_properties,
        }),
        headers=remove_none_values({
            "Content-Type": "application/json",
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            ),
        }),
        content=content,
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(requires_auth=OAuth2(id="arcade-asana"))
async def get_project_brief(
    context: ToolContext,
    project_brief_identifier: Annotated[
        str, "Globally unique identifier for the specific project brief to retrieve."
    ],
    include_optional_properties: Annotated[
        list[str] | None,
        "An array of property names to include in the response. These properties are excluded by default.",  # noqa: E501
    ] = None,
    pretty_output_enabled: Annotated[
        bool | None,
        "Enable readable, formatted output. Suitable for debugging. Increases response time and size.",  # noqa: E501
    ] = None,
) -> Annotated[dict[str, Any], "Response from the API endpoint 'getProjectBrief'."]:
    """Retrieve the full record for a project brief.

    Use this tool to obtain the complete details of a specific project brief by its unique identifier. Ideal for accessing comprehensive project information within Asana."""  # noqa: E501
    request_data: Any = None
    if isinstance(request_data, dict):
        request_data = remove_none_values(request_data)
    content = json.dumps(request_data) if request_data else None
    response = await make_request(
        url="https://app.asana.com/api/1.0/project_briefs/{project_brief_gid}".format(  # noqa: UP032
            project_brief_gid=project_brief_identifier
        ),
        method="GET",
        params=remove_none_values({
            "opt_pretty": pretty_output_enabled,
            "opt_fields": include_optional_properties,
        }),
        headers=remove_none_values({
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            )
        }),
        content=content,
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(requires_auth=OAuth2(id="arcade-asana"))
async def update_project_brief_asana(
    context: ToolContext,
    mode: Annotated[
        ToolMode,
        "Operation mode: 'get_request_schema' returns the OpenAPI spec "
        "for the request body, 'execute' performs the actual operation",
    ],
    project_brief_id: Annotated[
        str | None,
        "Globally unique identifier for the specific project brief to update.  Required when mode is 'execute', ignored when mode is 'get_request_schema'.",  # noqa: E501
    ] = None,
    include_optional_properties: Annotated[
        list[str] | None,
        "Specify a list of properties to include in the response, separated by commas.  Only used when mode is 'execute'.",  # noqa: E501
    ] = None,
    provide_pretty_output: Annotated[
        bool | None,
        "If true, the response is formatted with line breaks and indentation for readability. Use primarily for debugging.  Only used when mode is 'execute'.",  # noqa: E501
    ] = None,
    request_body: Annotated[
        str | None,
        "Stringified JSON representing the request body. Required when "
        "mode is 'execute', ignored when mode is 'get_request_schema'",
    ] = None,
) -> Annotated[dict[str, Any], "Response from the API endpoint 'updateProjectBrief'."]:
    """Update an Asana project brief.

    Use this tool to update an existing Asana project brief. Specify only the fields to be updated; fields not specified will remain unchanged. Returns the complete updated project brief record.

    Note: Understanding the request schema is necessary to properly create
    the stringified JSON input object for execution.\n\nThis operation also requires path, query parameters.

    Modes:
    - GET_REQUEST_SCHEMA: Returns the schema. Only call if you don't
      already have it. Do NOT call repeatedly if you already received
      the schema.
    - EXECUTE: Performs the operation with the provided request body
      JSON.\n      Note: You must also provide the required path, query parameters when executing.

    If you need the schema, call with mode='get_request_schema' ONCE, then execute.
    """  # noqa: E501
    if mode == ToolMode.GET_REQUEST_SCHEMA:
        return {
            "request_body_schema": REQUEST_BODY_SCHEMAS["UPDATEPROJECTBRIEFASANA"],
            "instructions": (
                "Use the request_body_schema to construct a valid JSON object. "
                "Once you have populated the object following the schema "
                "structure and requirements, call this tool again with "
                "mode='execute' and the stringified JSON as the "
                "request_body parameter along with the required path, query parameters. "
                "Do NOT call the schema mode again - you already have "
                "the schema now."
            ),
        }

    # Mode is EXECUTE - validate parameters
    # Validate required parameters
    missing_params = []
    if not project_brief_id:
        missing_params.append(("project_brief_id", "path"))

    if missing_params:
        param_names = [p[0] for p in missing_params]
        param_details = ", ".join([f"{p[0]} ({p[1]})" for p in missing_params])
        raise RetryableToolError(
            message=f"Missing required parameters: {param_names}",
            developer_message=(f"Required parameters validation failed: {param_details}"),
            additional_prompt_content=(
                f"The following required parameters are missing: "
                f"{param_details}. Please call this tool again with all "
                "required parameters."
            ),
        )

    # Validate request body is provided (not None or empty string)
    # Note: Empty objects like {} are allowed - schema validation will check if valid
    if request_body is None or request_body.strip() == "":
        raise RetryableToolError(
            message="Request body is required when mode is 'execute'",
            developer_message="The request_body parameter was null or empty string",
            additional_prompt_content=(
                "The request body is required to perform this operation. "
                "Use the schema below to construct a valid JSON object, "
                "then call this tool again in execute mode with the "
                "stringified JSON as the request_body parameter.\n\n"
                "Schema:\n\n```json\n" + REQUEST_BODY_SCHEMAS["UPDATEPROJECTBRIEFASANA"] + "\n```"
            ),
        )

    # Parse JSON
    try:
        request_data = json.loads(request_body)
    except json.JSONDecodeError as e:
        raise RetryableToolError(
            message=f"Invalid JSON in request body: {e!s}",
            developer_message=f"JSON parsing failed: {e!s}",
            additional_prompt_content=(
                f"The request body contains invalid JSON. Error: {e!s}\n\n"
                "Please provide a valid JSON string that matches the schema "
                "below, then call this tool again in execute mode.\n\n"
                "Schema:\n\n```json\n" + REQUEST_BODY_SCHEMAS["UPDATEPROJECTBRIEFASANA"] + "\n```"
            ),
        ) from e

    response = await make_request_with_schema_validation(
        url="https://app.asana.com/api/1.0/project_briefs/{project_brief_gid}".format(  # noqa: UP032
            project_brief_gid=project_brief_id
        ),
        method="PUT",
        request_data=request_data,
        schema=REQUEST_BODY_SCHEMAS["UPDATEPROJECTBRIEFASANA"],
        params=remove_none_values({
            "opt_pretty": provide_pretty_output,
            "opt_fields": include_optional_properties,
        }),
        headers=remove_none_values({
            "Content-Type": "application/json",
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            ),
        }),
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(requires_auth=OAuth2(id="arcade-asana"))
async def delete_project_brief(
    context: ToolContext,
    project_brief_unique_id: Annotated[
        str, "Globally unique identifier for the project brief to be deleted."
    ],
    enable_pretty_output: Annotated[
        bool | None,
        "Set to true for pretty-formatted output, making it more readable. Useful for debugging, this may increase response time and size.",  # noqa: E501
    ] = None,
) -> Annotated[dict[str, Any], "Response from the API endpoint 'deleteProjectBrief'."]:
    """Delete a specific project brief in Asana.

    Use this tool to delete an existing project brief in Asana by providing its unique identifier. The operation returns confirmation of deletion."""  # noqa: E501
    request_data: Any = None
    if isinstance(request_data, dict):
        request_data = remove_none_values(request_data)
    content = json.dumps(request_data) if request_data else None
    response = await make_request(
        url="https://app.asana.com/api/1.0/project_briefs/{project_brief_gid}".format(  # noqa: UP032
            project_brief_gid=project_brief_unique_id
        ),
        method="DELETE",
        params=remove_none_values({"opt_pretty": enable_pretty_output}),
        headers=remove_none_values({
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            )
        }),
        content=content,
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(requires_auth=OAuth2(id="arcade-asana"))
async def create_project_brief(
    context: ToolContext,
    mode: Annotated[
        ToolMode,
        "Operation mode: 'get_request_schema' returns the OpenAPI spec "
        "for the request body, 'execute' performs the actual operation",
    ],
    project_global_id: Annotated[
        str | None,
        "Globally unique identifier for the project in which the brief will be created.  Required when mode is 'execute', ignored when mode is 'get_request_schema'.",  # noqa: E501
    ] = None,
    include_optional_properties: Annotated[
        list[str] | None,
        "List of properties to include that are excluded by default in the response. Provide as an array of strings.  Only used when mode is 'execute'.",  # noqa: E501
    ] = None,
    enable_pretty_output: Annotated[
        bool | None,
        "Set to true to format the response with line breaks and indentation for readability. Useful for debugging.  Only used when mode is 'execute'.",  # noqa: E501
    ] = None,
    request_body: Annotated[
        str | None,
        "Stringified JSON representing the request body. Required when "
        "mode is 'execute', ignored when mode is 'get_request_schema'",
    ] = None,
) -> Annotated[dict[str, Any], "Response from the API endpoint 'createProjectBrief'."]:
    """Create a new project brief in Asana.

    Use this tool to create and initialize a new project brief within a specified Asana project. It returns the full details of the project brief after creation.

    Note: Understanding the request schema is necessary to properly create
    the stringified JSON input object for execution.\n\nThis operation also requires path, query parameters.

    Modes:
    - GET_REQUEST_SCHEMA: Returns the schema. Only call if you don't
      already have it. Do NOT call repeatedly if you already received
      the schema.
    - EXECUTE: Performs the operation with the provided request body
      JSON.\n      Note: You must also provide the required path, query parameters when executing.

    If you need the schema, call with mode='get_request_schema' ONCE, then execute.
    """  # noqa: E501
    if mode == ToolMode.GET_REQUEST_SCHEMA:
        return {
            "request_body_schema": REQUEST_BODY_SCHEMAS["CREATEPROJECTBRIEF"],
            "instructions": (
                "Use the request_body_schema to construct a valid JSON object. "
                "Once you have populated the object following the schema "
                "structure and requirements, call this tool again with "
                "mode='execute' and the stringified JSON as the "
                "request_body parameter along with the required path, query parameters. "
                "Do NOT call the schema mode again - you already have "
                "the schema now."
            ),
        }

    # Mode is EXECUTE - validate parameters
    # Validate required parameters
    missing_params = []
    if not project_global_id:
        missing_params.append(("project_global_id", "path"))

    if missing_params:
        param_names = [p[0] for p in missing_params]
        param_details = ", ".join([f"{p[0]} ({p[1]})" for p in missing_params])
        raise RetryableToolError(
            message=f"Missing required parameters: {param_names}",
            developer_message=(f"Required parameters validation failed: {param_details}"),
            additional_prompt_content=(
                f"The following required parameters are missing: "
                f"{param_details}. Please call this tool again with all "
                "required parameters."
            ),
        )

    # Validate request body is provided (not None or empty string)
    # Note: Empty objects like {} are allowed - schema validation will check if valid
    if request_body is None or request_body.strip() == "":
        raise RetryableToolError(
            message="Request body is required when mode is 'execute'",
            developer_message="The request_body parameter was null or empty string",
            additional_prompt_content=(
                "The request body is required to perform this operation. "
                "Use the schema below to construct a valid JSON object, "
                "then call this tool again in execute mode with the "
                "stringified JSON as the request_body parameter.\n\n"
                "Schema:\n\n```json\n" + REQUEST_BODY_SCHEMAS["CREATEPROJECTBRIEF"] + "\n```"
            ),
        )

    # Parse JSON
    try:
        request_data = json.loads(request_body)
    except json.JSONDecodeError as e:
        raise RetryableToolError(
            message=f"Invalid JSON in request body: {e!s}",
            developer_message=f"JSON parsing failed: {e!s}",
            additional_prompt_content=(
                f"The request body contains invalid JSON. Error: {e!s}\n\n"
                "Please provide a valid JSON string that matches the schema "
                "below, then call this tool again in execute mode.\n\n"
                "Schema:\n\n```json\n" + REQUEST_BODY_SCHEMAS["CREATEPROJECTBRIEF"] + "\n```"
            ),
        ) from e

    response = await make_request_with_schema_validation(
        url="https://app.asana.com/api/1.0/projects/{project_gid}/project_briefs".format(  # noqa: UP032
            project_gid=project_global_id
        ),
        method="POST",
        request_data=request_data,
        schema=REQUEST_BODY_SCHEMAS["CREATEPROJECTBRIEF"],
        params=remove_none_values({
            "opt_pretty": enable_pretty_output,
            "opt_fields": include_optional_properties,
        }),
        headers=remove_none_values({
            "Content-Type": "application/json",
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            ),
        }),
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(requires_auth=OAuth2(id="arcade-asana"))
async def get_project_membership_details(
    context: ToolContext,
    project_membership_id: Annotated[
        str, "The unique identifier for the project membership to retrieve details for."
    ],
    enable_pretty_output: Annotated[
        bool | None, "Set to true for a human-readable response format, useful for debugging."
    ] = None,
    include_optional_properties: Annotated[
        list[str] | None,
        "A list of optional project membership properties to include in the response. These are normally excluded by default.",  # noqa: E501
    ] = None,
) -> Annotated[dict[str, Any], "Response from the API endpoint 'getProjectMembership'."]:
    """Retrieve detailed information for a project membership in Asana.

    Call this tool to get a detailed record of a specific project membership in Asana by providing the project membership ID."""  # noqa: E501
    request_data: Any = None
    if isinstance(request_data, dict):
        request_data = remove_none_values(request_data)
    content = json.dumps(request_data) if request_data else None
    response = await make_request(
        url="https://app.asana.com/api/1.0/project_memberships/{project_membership_gid}".format(  # noqa: UP032
            project_membership_gid=project_membership_id
        ),
        method="GET",
        params=remove_none_values({
            "opt_pretty": enable_pretty_output,
            "opt_fields": include_optional_properties,
        }),
        headers=remove_none_values({
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            )
        }),
        content=content,
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(requires_auth=OAuth2(id="arcade-asana"))
async def get_project_memberships(
    context: ToolContext,
    project_unique_identifier: Annotated[
        str, "Globally unique identifier for the project to fetch memberships for."
    ],
    enable_pretty_output: Annotated[
        bool | None,
        "Enable pretty formatting for the response output. Useful for debugging as it adds line breaks and indentation, making it more readable. Note: This increases response size and time.",  # noqa: E501
    ] = None,
    include_optional_properties: Annotated[
        list[str] | None,
        "List of properties to include in the response. Use a comma-separated list to specify optional properties to be included in the response.",  # noqa: E501
    ] = None,
    page_offset_token: Annotated[
        str | None,
        "Token for pagination to retrieve the next set of results. Use the token from a previous response to get subsequent pages.",  # noqa: E501
    ] = None,
    results_per_page: Annotated[
        int | None, "The number of objects to return per page (between 1 and 100)."
    ] = None,
    user_identifier: Annotated[
        str | None, 'A string identifying a user, either "me", an email, or the user\'s gid.'
    ] = None,
) -> Annotated[dict[str, Any], "Response from the API endpoint 'getProjectMembershipsForProject'."]:
    """Fetch project membership records from Asana.

    Retrieve compact information about project memberships for a specified project in Asana."""
    request_data: Any = None
    if isinstance(request_data, dict):
        request_data = remove_none_values(request_data)
    content = json.dumps(request_data) if request_data else None
    response = await make_request(
        url="https://app.asana.com/api/1.0/projects/{project_gid}/project_memberships".format(  # noqa: UP032
            project_gid=project_unique_identifier
        ),
        method="GET",
        params=remove_none_values({
            "user": user_identifier,
            "opt_pretty": enable_pretty_output,
            "limit": results_per_page,
            "offset": page_offset_token,
            "opt_fields": include_optional_properties,
        }),
        headers=remove_none_values({
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            )
        }),
        content=content,
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(requires_auth=OAuth2(id="arcade-asana"))
async def get_project_status_update(
    context: ToolContext,
    project_status_update_id: Annotated[
        str,
        "The ID of the project status update to retrieve. This should be a unique identifier for the specific status update.",  # noqa: E501
    ],
    enable_pretty_output: Annotated[
        bool | None,
        "If true, formats the response with line breaks and indentation for readability. Recommended for debugging only, as it increases response size.",  # noqa: E501
    ] = None,
    include_optional_properties: Annotated[
        list[str] | None,
        "A list of optional properties to include in the response. This allows fetching additional details that are excluded by default.",  # noqa: E501
    ] = None,
) -> Annotated[dict[str, Any], "Response from the API endpoint 'getProjectStatus'."]:
    """Fetches a complete status update record for a project.

    Retrieves the full details of a specific project status update. Note that this endpoint is deprecated, and new integrations should use `/status_updates/{status_gid}` instead."""  # noqa: E501
    request_data: Any = None
    if isinstance(request_data, dict):
        request_data = remove_none_values(request_data)
    content = json.dumps(request_data) if request_data else None
    response = await make_request(
        url="https://app.asana.com/api/1.0/project_statuses/{project_status_gid}".format(  # noqa: UP032
            project_status_gid=project_status_update_id
        ),
        method="GET",
        params=remove_none_values({
            "opt_pretty": enable_pretty_output,
            "opt_fields": include_optional_properties,
        }),
        headers=remove_none_values({
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            )
        }),
        content=content,
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(requires_auth=OAuth2(id="arcade-asana"))
async def delete_project_status(
    context: ToolContext,
    project_status_id: Annotated[
        str, "The identifier for the project status update you wish to delete."
    ],
    pretty_output: Annotated[
        bool | None,
        "Enable pretty output for readable formatting during debugging; increases response size and time.",  # noqa: E501
    ] = None,
) -> Annotated[dict[str, Any], "Response from the API endpoint 'deleteProjectStatus'."]:
    """Delete a specific project status update in Asana.

    This tool deletes an existing project status update in Asana. It should be called when you need to remove a specific status update from a project. Note: This endpoint is deprecated; consider using the `/status_updates/{status_gid}` route for new integrations."""  # noqa: E501
    request_data: Any = None
    if isinstance(request_data, dict):
        request_data = remove_none_values(request_data)
    content = json.dumps(request_data) if request_data else None
    response = await make_request(
        url="https://app.asana.com/api/1.0/project_statuses/{project_status_gid}".format(  # noqa: UP032
            project_status_gid=project_status_id
        ),
        method="DELETE",
        params=remove_none_values({"opt_pretty": pretty_output}),
        headers=remove_none_values({
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            )
        }),
        content=content,
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(requires_auth=OAuth2(id="arcade-asana"))
async def get_project_status_updates(
    context: ToolContext,
    project_global_identifier: Annotated[
        str, "Globally unique identifier for the project in Asana."
    ],
    enable_pretty_output: Annotated[
        bool | None,
        "Enable to format the response for readability with line breaks and indentation. Only recommended for debugging due to increased response size and processing time.",  # noqa: E501
    ] = None,
    include_optional_fields: Annotated[
        list[str] | None, "Comma-separated list of optional properties to include in the response."
    ] = None,
    pagination_offset_token: Annotated[
        str | None,
        "Offset token for pagination. Use the token provided by a previous request to retrieve the next page of results.",  # noqa: E501
    ] = None,
    results_per_page: Annotated[
        int | None, "Number of status updates to return per page, must be between 1 and 100."
    ] = None,
) -> Annotated[dict[str, Any], "Response from the API endpoint 'getProjectStatusesForProject'."]:
    """Fetch compact status updates for a given project.

    Use this tool to retrieve all the compact status update records for a specified project on Asana. This is useful to get a historical or current view of the project's status updates. Note that this endpoint is deprecated for new integrations."""  # noqa: E501
    request_data: Any = None
    if isinstance(request_data, dict):
        request_data = remove_none_values(request_data)
    content = json.dumps(request_data) if request_data else None
    response = await make_request(
        url="https://app.asana.com/api/1.0/projects/{project_gid}/project_statuses".format(  # noqa: UP032
            project_gid=project_global_identifier
        ),
        method="GET",
        params=remove_none_values({
            "opt_pretty": enable_pretty_output,
            "limit": results_per_page,
            "offset": pagination_offset_token,
            "opt_fields": include_optional_fields,
        }),
        headers=remove_none_values({
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            )
        }),
        content=content,
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(requires_auth=OAuth2(id="arcade-asana"))
async def create_project_status_update(
    context: ToolContext,
    mode: Annotated[
        ToolMode,
        "Operation mode: 'get_request_schema' returns the OpenAPI spec "
        "for the request body, 'execute' performs the actual operation",
    ],
    project_global_id: Annotated[
        str | None,
        "Globally unique identifier for the project in Asana.  Required when mode is 'execute', ignored when mode is 'get_request_schema'.",  # noqa: E501
    ] = None,
    included_optional_properties: Annotated[
        list[str] | None,
        "Comma-separated list of optional properties to include in the response, which are excluded by default.  Only used when mode is 'execute'.",  # noqa: E501
    ] = None,
    enable_pretty_output: Annotated[
        bool | None,
        "If true, format the response for readability with line breaks and indentation. Mainly for debugging purposes.  Only used when mode is 'execute'.",  # noqa: E501
    ] = None,
    request_body: Annotated[
        str | None,
        "Stringified JSON representing the request body. Required when "
        "mode is 'execute', ignored when mode is 'get_request_schema'",
    ] = None,
) -> Annotated[dict[str, Any], "Response from the API endpoint 'createProjectStatusForProject'."]:
    """Creates a new status update for a project.

    This tool creates a new status update on a specified project in Asana and returns the full record of the newly created project status update. Note: Prefer using the `/status_updates` route for new integrations.

    Note: Understanding the request schema is necessary to properly create
    the stringified JSON input object for execution.\n\nThis operation also requires path, query parameters.

    Modes:
    - GET_REQUEST_SCHEMA: Returns the schema. Only call if you don't
      already have it. Do NOT call repeatedly if you already received
      the schema.
    - EXECUTE: Performs the operation with the provided request body
      JSON.\n      Note: You must also provide the required path, query parameters when executing.

    If you need the schema, call with mode='get_request_schema' ONCE, then execute.
    """  # noqa: E501
    if mode == ToolMode.GET_REQUEST_SCHEMA:
        return {
            "request_body_schema": REQUEST_BODY_SCHEMAS["CREATEPROJECTSTATUSUPDATE"],
            "instructions": (
                "Use the request_body_schema to construct a valid JSON object. "
                "Once you have populated the object following the schema "
                "structure and requirements, call this tool again with "
                "mode='execute' and the stringified JSON as the "
                "request_body parameter along with the required path, query parameters. "
                "Do NOT call the schema mode again - you already have "
                "the schema now."
            ),
        }

    # Mode is EXECUTE - validate parameters
    # Validate required parameters
    missing_params = []
    if not project_global_id:
        missing_params.append(("project_global_id", "path"))

    if missing_params:
        param_names = [p[0] for p in missing_params]
        param_details = ", ".join([f"{p[0]} ({p[1]})" for p in missing_params])
        raise RetryableToolError(
            message=f"Missing required parameters: {param_names}",
            developer_message=(f"Required parameters validation failed: {param_details}"),
            additional_prompt_content=(
                f"The following required parameters are missing: "
                f"{param_details}. Please call this tool again with all "
                "required parameters."
            ),
        )

    # Validate request body is provided (not None or empty string)
    # Note: Empty objects like {} are allowed - schema validation will check if valid
    if request_body is None or request_body.strip() == "":
        raise RetryableToolError(
            message="Request body is required when mode is 'execute'",
            developer_message="The request_body parameter was null or empty string",
            additional_prompt_content=(
                "The request body is required to perform this operation. "
                "Use the schema below to construct a valid JSON object, "
                "then call this tool again in execute mode with the "
                "stringified JSON as the request_body parameter.\n\n"
                "Schema:\n\n```json\n" + REQUEST_BODY_SCHEMAS["CREATEPROJECTSTATUSUPDATE"] + "\n```"
            ),
        )

    # Parse JSON
    try:
        request_data = json.loads(request_body)
    except json.JSONDecodeError as e:
        raise RetryableToolError(
            message=f"Invalid JSON in request body: {e!s}",
            developer_message=f"JSON parsing failed: {e!s}",
            additional_prompt_content=(
                f"The request body contains invalid JSON. Error: {e!s}\n\n"
                "Please provide a valid JSON string that matches the schema "
                "below, then call this tool again in execute mode.\n\n"
                "Schema:\n\n```json\n" + REQUEST_BODY_SCHEMAS["CREATEPROJECTSTATUSUPDATE"] + "\n```"
            ),
        ) from e

    response = await make_request_with_schema_validation(
        url="https://app.asana.com/api/1.0/projects/{project_gid}/project_statuses".format(  # noqa: UP032
            project_gid=project_global_id
        ),
        method="POST",
        request_data=request_data,
        schema=REQUEST_BODY_SCHEMAS["CREATEPROJECTSTATUSUPDATE"],
        params=remove_none_values({
            "opt_pretty": enable_pretty_output,
            "opt_fields": included_optional_properties,
        }),
        headers=remove_none_values({
            "Content-Type": "application/json",
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            ),
        }),
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(requires_auth=OAuth2(id="arcade-asana", scopes=["project_templates:read"]))
async def get_project_template_details(
    context: ToolContext,
    project_template_id: Annotated[
        str, "Globally unique identifier for the project template to fetch complete details."
    ],
    include_optional_properties: Annotated[
        list[str] | None,
        "List properties to include in the response. Exclude default properties, using a comma-separated format.",  # noqa: E501
    ] = None,
    pretty_output_enabled: Annotated[
        bool | None,
        "Set to true to receive a formatted and readable response output, suitable for debugging. Increases response time and size.",  # noqa: E501
    ] = None,
) -> Annotated[dict[str, Any], "Response from the API endpoint 'getProjectTemplate'."]:
    """Retrieve complete details of a specific Asana project template.

    Fetches all information about a particular project template in Asana. Useful for obtaining full details of a project's structure and specifications."""  # noqa: E501
    request_data: Any = None
    if isinstance(request_data, dict):
        request_data = remove_none_values(request_data)
    content = json.dumps(request_data) if request_data else None
    response = await make_request(
        url="https://app.asana.com/api/1.0/project_templates/{project_template_gid}".format(  # noqa: UP032
            project_template_gid=project_template_id
        ),
        method="GET",
        params=remove_none_values({
            "opt_pretty": pretty_output_enabled,
            "opt_fields": include_optional_properties,
        }),
        headers=remove_none_values({
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            )
        }),
        content=content,
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(requires_auth=OAuth2(id="arcade-asana"))
async def delete_project_template(
    context: ToolContext,
    project_template_identifier: Annotated[
        str, "Globally unique identifier for the Asana project template to delete."
    ],
    enable_pretty_output: Annotated[
        bool | None,
        "If true, outputs the response in a readable, pretty format with line breaks and indentation, mainly for debugging purposes.",  # noqa: E501
    ] = None,
) -> Annotated[dict[str, Any], "Response from the API endpoint 'deleteProjectTemplate'."]:
    """Delete a specific existing project template in Asana.

    Use this tool to permanently delete a specific project template in Asana by providing the template's unique identifier."""  # noqa: E501
    request_data: Any = None
    if isinstance(request_data, dict):
        request_data = remove_none_values(request_data)
    content = json.dumps(request_data) if request_data else None
    response = await make_request(
        url="https://app.asana.com/api/1.0/project_templates/{project_template_gid}".format(  # noqa: UP032
            project_template_gid=project_template_identifier
        ),
        method="DELETE",
        params=remove_none_values({"opt_pretty": enable_pretty_output}),
        headers=remove_none_values({
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            )
        }),
        content=content,
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(requires_auth=OAuth2(id="arcade-asana", scopes=["project_templates:read"]))
async def get_asana_project_templates(
    context: ToolContext,
    include_optional_fields: Annotated[
        list[str] | None,
        "A list of optional properties to include in the response. Provide as an array of strings, with each string representing a property name.",  # noqa: E501
    ] = None,
    pagination_offset_token: Annotated[
        str | None,
        "Offset token for pagination. Use the token returned from a previous request to fetch the next page of results. If omitted, the first page is returned.",  # noqa: E501
    ] = None,
    pretty_output: Annotated[
        bool | None,
        "Set to true to enable pretty JSON formatting for debugging, despite increased size and time.",  # noqa: E501
    ] = None,
    results_per_page: Annotated[
        int | None, "Specify the number of project templates to return per page, between 1 and 100."
    ] = None,
    team_filter: Annotated[str | None, "The team ID to filter project templates on."] = None,
    workspace_identifier: Annotated[
        str | None, "The identifier of the workspace to filter project templates results on."
    ] = None,
) -> Annotated[dict[str, Any], "Response from the API endpoint 'getProjectTemplates'."]:
    """Fetch project template records from Asana.

    Retrieve compact project template records for all templates in a specified team or workspace within Asana. This requires the 'project_templates:read' scope."""  # noqa: E501
    request_data: Any = None
    if isinstance(request_data, dict):
        request_data = remove_none_values(request_data)
    content = json.dumps(request_data) if request_data else None
    response = await make_request(
        url="https://app.asana.com/api/1.0/project_templates",
        method="GET",
        params=remove_none_values({
            "opt_pretty": pretty_output,
            "workspace": workspace_identifier,
            "team": team_filter,
            "limit": results_per_page,
            "offset": pagination_offset_token,
            "opt_fields": include_optional_fields,
        }),
        headers=remove_none_values({
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            )
        }),
        content=content,
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(requires_auth=OAuth2(id="arcade-asana", scopes=["project_templates:read"]))
async def get_project_templates_for_team(
    context: ToolContext,
    team_id: Annotated[
        str, "Globally unique identifier for the team to retrieve project templates."
    ],
    enable_pretty_output: Annotated[
        bool | None,
        'Enable to get the response in a readable, "pretty" JSON format. Useful for debugging.',
    ] = None,
    include_optional_properties: Annotated[
        list[str] | None,
        "Comma-separated list of optional properties to include in the response. Specify any properties excluded by default that you wish to see.",  # noqa: E501
    ] = None,
    pagination_offset_token: Annotated[
        str | None,
        "Offset token to fetch the next page of results. Use the token returned from a previous request's pagination.",  # noqa: E501
    ] = None,
    results_per_page: Annotated[
        int | None, "Number of project templates to return per page. Must be between 1 and 100."
    ] = None,
) -> Annotated[dict[str, Any], "Response from the API endpoint 'getProjectTemplatesForTeam'."]:
    """Retrieve compact project template records for a team.

    Call this tool to get a list of project templates for a specified team in Asana. It requires the 'project_templates:read' scope."""  # noqa: E501
    request_data: Any = None
    if isinstance(request_data, dict):
        request_data = remove_none_values(request_data)
    content = json.dumps(request_data) if request_data else None
    response = await make_request(
        url="https://app.asana.com/api/1.0/teams/{team_gid}/project_templates".format(  # noqa: UP032
            team_gid=team_id
        ),
        method="GET",
        params=remove_none_values({
            "opt_pretty": enable_pretty_output,
            "limit": results_per_page,
            "offset": pagination_offset_token,
            "opt_fields": include_optional_properties,
        }),
        headers=remove_none_values({
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            )
        }),
        content=content,
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(requires_auth=OAuth2(id="arcade-asana", scopes=["projects:write"]))
async def instantiate_project_from_template(
    context: ToolContext,
    mode: Annotated[
        ToolMode,
        "Operation mode: 'get_request_schema' returns the OpenAPI spec "
        "for the request body, 'execute' performs the actual operation",
    ],
    project_template_id: Annotated[
        str | None,
        "Globally unique identifier for the project template to instantiate the project from.  Required when mode is 'execute', ignored when mode is 'get_request_schema'.",  # noqa: E501
    ] = None,
    include_optional_properties: Annotated[
        list[str] | None,
        "A list of optional properties to include in the response. Provide as a comma-separated list.  Only used when mode is 'execute'.",  # noqa: E501
    ] = None,
    enable_pretty_output: Annotated[
        bool | None,
        "Enable pretty formatting for the response. Use this for debugging purposes as it increases response time and size.  Only used when mode is 'execute'.",  # noqa: E501
    ] = None,
    request_body: Annotated[
        str | None,
        "Stringified JSON representing the request body. Required when "
        "mode is 'execute', ignored when mode is 'get_request_schema'",
    ] = None,
) -> Annotated[dict[str, Any], "Response from the API endpoint 'instantiateProject'."]:
    """Asynchronously instantiate a project from a template.

    This tool creates a job to asynchronously handle project instantiation using a specified project template. It should be called when you need to create a new project from an existing template in Asana. Ensure you have the 'projects:write' scope and verify if your workspace is an organization.

    Note: Understanding the request schema is necessary to properly create
    the stringified JSON input object for execution.\n\nThis operation also requires path, query parameters.

    Modes:
    - GET_REQUEST_SCHEMA: Returns the schema. Only call if you don't
      already have it. Do NOT call repeatedly if you already received
      the schema.
    - EXECUTE: Performs the operation with the provided request body
      JSON.\n      Note: You must also provide the required path, query parameters when executing.

    If you need the schema, call with mode='get_request_schema' ONCE, then execute.
    """  # noqa: E501
    if mode == ToolMode.GET_REQUEST_SCHEMA:
        return {
            "request_body_schema": REQUEST_BODY_SCHEMAS["INSTANTIATEPROJECTFROMTEMPLATE"],
            "instructions": (
                "Use the request_body_schema to construct a valid JSON object. "
                "Once you have populated the object following the schema "
                "structure and requirements, call this tool again with "
                "mode='execute' and the stringified JSON as the "
                "request_body parameter along with the required path, query parameters. "
                "Do NOT call the schema mode again - you already have "
                "the schema now."
            ),
        }

    # Mode is EXECUTE - validate parameters
    # Validate required parameters
    missing_params = []
    if not project_template_id:
        missing_params.append(("project_template_id", "path"))

    if missing_params:
        param_names = [p[0] for p in missing_params]
        param_details = ", ".join([f"{p[0]} ({p[1]})" for p in missing_params])
        raise RetryableToolError(
            message=f"Missing required parameters: {param_names}",
            developer_message=(f"Required parameters validation failed: {param_details}"),
            additional_prompt_content=(
                f"The following required parameters are missing: "
                f"{param_details}. Please call this tool again with all "
                "required parameters."
            ),
        )

    # Validate request body is provided (not None or empty string)
    # Note: Empty objects like {} are allowed - schema validation will check if valid
    if request_body is None or request_body.strip() == "":
        raise RetryableToolError(
            message="Request body is required when mode is 'execute'",
            developer_message="The request_body parameter was null or empty string",
            additional_prompt_content=(
                "The request body is required to perform this operation. "
                "Use the schema below to construct a valid JSON object, "
                "then call this tool again in execute mode with the "
                "stringified JSON as the request_body parameter.\n\n"
                "Schema:\n\n```json\n"
                + REQUEST_BODY_SCHEMAS["INSTANTIATEPROJECTFROMTEMPLATE"]
                + "\n```"
            ),
        )

    # Parse JSON
    try:
        request_data = json.loads(request_body)
    except json.JSONDecodeError as e:
        raise RetryableToolError(
            message=f"Invalid JSON in request body: {e!s}",
            developer_message=f"JSON parsing failed: {e!s}",
            additional_prompt_content=(
                f"The request body contains invalid JSON. Error: {e!s}\n\n"
                "Please provide a valid JSON string that matches the schema "
                "below, then call this tool again in execute mode.\n\n"
                "Schema:\n\n```json\n"
                + REQUEST_BODY_SCHEMAS["INSTANTIATEPROJECTFROMTEMPLATE"]
                + "\n```"
            ),
        ) from e

    response = await make_request_with_schema_validation(
        url="https://app.asana.com/api/1.0/project_templates/{project_template_gid}/instantiateProject".format(  # noqa: UP032
            project_template_gid=project_template_id
        ),
        method="POST",
        request_data=request_data,
        schema=REQUEST_BODY_SCHEMAS["INSTANTIATEPROJECTFROMTEMPLATE"],
        params=remove_none_values({
            "opt_pretty": enable_pretty_output,
            "opt_fields": include_optional_properties,
        }),
        headers=remove_none_values({
            "Content-Type": "application/json",
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            ),
        }),
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(requires_auth=OAuth2(id="arcade-asana", scopes=["projects:read"]))
async def get_asana_projects(
    context: ToolContext,
    enable_pretty_output: Annotated[
        bool | None,
        "Enable pretty JSON formatting for debugging. Increases response size and processing time.",
    ] = None,
    optional_fields_to_include: Annotated[
        list[str] | None, "Comma-separated list of optional properties to include in the response."
    ] = None,
    pagination_offset_token: Annotated[
        str | None,
        "The offset token for pagination to obtain the next page of results. Use this token received from a previous request to continue retrieving subsequent data.",  # noqa: E501
    ] = None,
    results_per_page: Annotated[
        int | None, "Number of projects to return per page, from 1 to 100."
    ] = None,
    return_only_archived_projects: Annotated[
        bool | None,
        "Boolean to filter projects by their archived status. Setting this to true returns only archived projects.",  # noqa: E501
    ] = None,
    team_filter: Annotated[
        str | None, "Specify the team to filter projects in the Asana workspace."
    ] = None,
    workspace_filter: Annotated[
        str | None, "Specify the workspace or organization to filter the Asana projects."
    ] = None,
) -> Annotated[dict[str, Any], "Response from the API endpoint 'getProjects'."]:
    """Fetch filtered project records from Asana.

    Retrieve compact project records from Asana by applying optional filters for more efficient data handling, especially in large domains. Ensure the 'projects:read' scope is authorized."""  # noqa: E501
    request_data: Any = None
    if isinstance(request_data, dict):
        request_data = remove_none_values(request_data)
    content = json.dumps(request_data) if request_data else None
    response = await make_request(
        url="https://app.asana.com/api/1.0/projects",
        method="GET",
        params=remove_none_values({
            "opt_pretty": enable_pretty_output,
            "limit": results_per_page,
            "offset": pagination_offset_token,
            "workspace": workspace_filter,
            "team": team_filter,
            "archived": return_only_archived_projects,
            "opt_fields": optional_fields_to_include,
        }),
        headers=remove_none_values({
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            )
        }),
        content=content,
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(requires_auth=OAuth2(id="arcade-asana", scopes=["projects:write"]))
async def create_new_asana_project(
    context: ToolContext,
    mode: Annotated[
        ToolMode,
        "Operation mode: 'get_request_schema' returns the OpenAPI spec "
        "for the request body, 'execute' performs the actual operation",
    ],
    included_properties_list: Annotated[
        list[str] | None,
        "List of properties to include in the response. Specify as a comma-separated list to include optional fields that are excluded by default.  Only used when mode is 'execute'.",  # noqa: E501
    ] = None,
    enable_pretty_output: Annotated[
        bool | None,
        "Set to true for readable JSON formatting with indents and line breaks. This is useful for debugging but increases response size and time.  Only used when mode is 'execute'.",  # noqa: E501
    ] = None,
    request_body: Annotated[
        str | None,
        "Stringified JSON representing the request body. Required when "
        "mode is 'execute', ignored when mode is 'get_request_schema'",
    ] = None,
) -> Annotated[dict[str, Any], "Response from the API endpoint 'createProject'."]:
    """Create a new project in an Asana workspace or team.

    This tool creates a new project in a specified workspace or team within Asana. It is essential to specify the workspace, and if applicable, the team, especially when the workspace is an organization. Once set, these cannot be changed. Use this tool to organize tasks and initiatives within Asana projects.

    Note: Understanding the request schema is necessary to properly create
    the stringified JSON input object for execution.\n\nThis operation also requires query parameters.

    Modes:
    - GET_REQUEST_SCHEMA: Returns the schema. Only call if you don't
      already have it. Do NOT call repeatedly if you already received
      the schema.
    - EXECUTE: Performs the operation with the provided request body
      JSON.\n      Note: You must also provide the required query parameters when executing.

    If you need the schema, call with mode='get_request_schema' ONCE, then execute.
    """  # noqa: E501
    if mode == ToolMode.GET_REQUEST_SCHEMA:
        return {
            "request_body_schema": REQUEST_BODY_SCHEMAS["CREATENEWASANAPROJECT"],
            "instructions": (
                "Use the request_body_schema to construct a valid JSON object. "
                "Once you have populated the object following the schema "
                "structure and requirements, call this tool again with "
                "mode='execute' and the stringified JSON as the "
                "request_body parameter along with the required query parameters. "
                "Do NOT call the schema mode again - you already have "
                "the schema now."
            ),
        }

    # Mode is EXECUTE - validate parameters

    # Validate request body is provided (not None or empty string)
    # Note: Empty objects like {} are allowed - schema validation will check if valid
    if request_body is None or request_body.strip() == "":
        raise RetryableToolError(
            message="Request body is required when mode is 'execute'",
            developer_message="The request_body parameter was null or empty string",
            additional_prompt_content=(
                "The request body is required to perform this operation. "
                "Use the schema below to construct a valid JSON object, "
                "then call this tool again in execute mode with the "
                "stringified JSON as the request_body parameter.\n\n"
                "Schema:\n\n```json\n" + REQUEST_BODY_SCHEMAS["CREATENEWASANAPROJECT"] + "\n```"
            ),
        )

    # Parse JSON
    try:
        request_data = json.loads(request_body)
    except json.JSONDecodeError as e:
        raise RetryableToolError(
            message=f"Invalid JSON in request body: {e!s}",
            developer_message=f"JSON parsing failed: {e!s}",
            additional_prompt_content=(
                f"The request body contains invalid JSON. Error: {e!s}\n\n"
                "Please provide a valid JSON string that matches the schema "
                "below, then call this tool again in execute mode.\n\n"
                "Schema:\n\n```json\n" + REQUEST_BODY_SCHEMAS["CREATENEWASANAPROJECT"] + "\n```"
            ),
        ) from e

    response = await make_request_with_schema_validation(
        url="https://app.asana.com/api/1.0/projects",
        method="POST",
        request_data=request_data,
        schema=REQUEST_BODY_SCHEMAS["CREATENEWASANAPROJECT"],
        params=remove_none_values({
            "opt_pretty": enable_pretty_output,
            "opt_fields": included_properties_list,
        }),
        headers=remove_none_values({
            "Content-Type": "application/json",
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            ),
        }),
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(requires_auth=OAuth2(id="arcade-asana", scopes=["projects:read"]))
async def get_project_details(
    context: ToolContext,
    project_global_identifier: Annotated[
        str, "A unique identifier for the Asana project you want to retrieve details for."
    ],
    enable_pretty_output: Annotated[
        bool | None,
        "Set to true for nicely formatted, readable output, mainly for debugging due to increased size and time.",  # noqa: E501
    ] = None,
    include_optional_properties: Annotated[
        list[str] | None,
        "A list of optional property names to include in the project details response. Provide these as a list of strings.",  # noqa: E501
    ] = None,
) -> Annotated[dict[str, Any], "Response from the API endpoint 'getProject'."]:
    """Retrieve complete details of a specified Asana project.

    This tool retrieves the complete record of a specific project from Asana. It requires 'projects:read' scope and additional scopes for accessing custom field settings and team information, if needed."""  # noqa: E501
    request_data: Any = None
    if isinstance(request_data, dict):
        request_data = remove_none_values(request_data)
    content = json.dumps(request_data) if request_data else None
    response = await make_request(
        url="https://app.asana.com/api/1.0/projects/{project_gid}".format(  # noqa: UP032
            project_gid=project_global_identifier
        ),
        method="GET",
        params=remove_none_values({
            "opt_pretty": enable_pretty_output,
            "opt_fields": include_optional_properties,
        }),
        headers=remove_none_values({
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            )
        }),
        content=content,
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(requires_auth=OAuth2(id="arcade-asana", scopes=["projects:write"]))
async def update_project_details(
    context: ToolContext,
    mode: Annotated[
        ToolMode,
        "Operation mode: 'get_request_schema' returns the OpenAPI spec "
        "for the request body, 'execute' performs the actual operation",
    ],
    project_global_id: Annotated[
        str | None,
        "Globally unique identifier for the project to be updated.  Required when mode is 'execute', ignored when mode is 'get_request_schema'.",  # noqa: E501
    ] = None,
    include_optional_properties: Annotated[
        list[str] | None,
        "Comma-separated list of properties to include in the response, showing optional fields by default excluded.  Only used when mode is 'execute'.",  # noqa: E501
    ] = None,
    enable_pretty_output: Annotated[
        bool | None,
        "Enable pretty formatting for the response output, making it more readable with line breaks and indentation. Use this primarily for debugging as it increases response size and processing time.  Only used when mode is 'execute'.",  # noqa: E501
    ] = None,
    request_body: Annotated[
        str | None,
        "Stringified JSON representing the request body. Required when "
        "mode is 'execute', ignored when mode is 'get_request_schema'",
    ] = None,
) -> Annotated[dict[str, Any], "Response from the API endpoint 'updateProject'."]:
    """Update specific fields of an existing project.

    Use this tool to update specific fields of an existing project in Asana. Specify only the fields you wish to change to avoid overwriting changes made by others. Returns the fully updated project record.

    Note: Understanding the request schema is necessary to properly create
    the stringified JSON input object for execution.\n\nThis operation also requires path, query parameters.

    Modes:
    - GET_REQUEST_SCHEMA: Returns the schema. Only call if you don't
      already have it. Do NOT call repeatedly if you already received
      the schema.
    - EXECUTE: Performs the operation with the provided request body
      JSON.\n      Note: You must also provide the required path, query parameters when executing.

    If you need the schema, call with mode='get_request_schema' ONCE, then execute.
    """  # noqa: E501
    if mode == ToolMode.GET_REQUEST_SCHEMA:
        return {
            "request_body_schema": REQUEST_BODY_SCHEMAS["UPDATEPROJECTDETAILS"],
            "instructions": (
                "Use the request_body_schema to construct a valid JSON object. "
                "Once you have populated the object following the schema "
                "structure and requirements, call this tool again with "
                "mode='execute' and the stringified JSON as the "
                "request_body parameter along with the required path, query parameters. "
                "Do NOT call the schema mode again - you already have "
                "the schema now."
            ),
        }

    # Mode is EXECUTE - validate parameters
    # Validate required parameters
    missing_params = []
    if not project_global_id:
        missing_params.append(("project_global_id", "path"))

    if missing_params:
        param_names = [p[0] for p in missing_params]
        param_details = ", ".join([f"{p[0]} ({p[1]})" for p in missing_params])
        raise RetryableToolError(
            message=f"Missing required parameters: {param_names}",
            developer_message=(f"Required parameters validation failed: {param_details}"),
            additional_prompt_content=(
                f"The following required parameters are missing: "
                f"{param_details}. Please call this tool again with all "
                "required parameters."
            ),
        )

    # Validate request body is provided (not None or empty string)
    # Note: Empty objects like {} are allowed - schema validation will check if valid
    if request_body is None or request_body.strip() == "":
        raise RetryableToolError(
            message="Request body is required when mode is 'execute'",
            developer_message="The request_body parameter was null or empty string",
            additional_prompt_content=(
                "The request body is required to perform this operation. "
                "Use the schema below to construct a valid JSON object, "
                "then call this tool again in execute mode with the "
                "stringified JSON as the request_body parameter.\n\n"
                "Schema:\n\n```json\n" + REQUEST_BODY_SCHEMAS["UPDATEPROJECTDETAILS"] + "\n```"
            ),
        )

    # Parse JSON
    try:
        request_data = json.loads(request_body)
    except json.JSONDecodeError as e:
        raise RetryableToolError(
            message=f"Invalid JSON in request body: {e!s}",
            developer_message=f"JSON parsing failed: {e!s}",
            additional_prompt_content=(
                f"The request body contains invalid JSON. Error: {e!s}\n\n"
                "Please provide a valid JSON string that matches the schema "
                "below, then call this tool again in execute mode.\n\n"
                "Schema:\n\n```json\n" + REQUEST_BODY_SCHEMAS["UPDATEPROJECTDETAILS"] + "\n```"
            ),
        ) from e

    response = await make_request_with_schema_validation(
        url="https://app.asana.com/api/1.0/projects/{project_gid}".format(  # noqa: UP032
            project_gid=project_global_id
        ),
        method="PUT",
        request_data=request_data,
        schema=REQUEST_BODY_SCHEMAS["UPDATEPROJECTDETAILS"],
        params=remove_none_values({
            "opt_pretty": enable_pretty_output,
            "opt_fields": include_optional_properties,
        }),
        headers=remove_none_values({
            "Content-Type": "application/json",
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            ),
        }),
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(requires_auth=OAuth2(id="arcade-asana", scopes=["projects:delete"]))
async def delete_asana_project(
    context: ToolContext,
    project_unique_identifier: Annotated[
        str, "Globally unique identifier for the project to be deleted in Asana."
    ],
    enable_pretty_output: Annotated[
        bool | None,
        "Enable pretty formatting for the response to make it more readable. It's recommended for debugging purposes only due to increased response size and time.",  # noqa: E501
    ] = None,
) -> Annotated[dict[str, Any], "Response from the API endpoint 'deleteProject'."]:
    """Delete a specific project in Asana.

    Use this tool to delete an existing project in Asana by providing the project's unique identifier. The tool is called when you need to remove a project from Asana, and it confirms the deletion without returning any additional data."""  # noqa: E501
    request_data: Any = None
    if isinstance(request_data, dict):
        request_data = remove_none_values(request_data)
    content = json.dumps(request_data) if request_data else None
    response = await make_request(
        url="https://app.asana.com/api/1.0/projects/{project_gid}".format(  # noqa: UP032
            project_gid=project_unique_identifier
        ),
        method="DELETE",
        params=remove_none_values({"opt_pretty": enable_pretty_output}),
        headers=remove_none_values({
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            )
        }),
        content=content,
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(requires_auth=OAuth2(id="arcade-asana", scopes=["projects:write"]))
async def asana_duplicate_project(
    context: ToolContext,
    project_global_id: Annotated[
        str, "Globally unique identifier for the project to be duplicated."
    ],
    include_elements_in_project_duplication: Annotated[
        str | None,
        "A comma-separated list of optional elements to include when duplicating a project. Some elements are auto-included and cannot be excluded.",  # noqa: E501
    ] = None,
    include_optional_fields: Annotated[
        list[str] | None,
        "Comma-separated list of optional properties to include in the response, such as additional project details.",  # noqa: E501
    ] = None,
    last_due_date_in_duplicated_project: Annotated[
        str | None,
        "Sets the last due date in the duplicated project. The subsequent due dates will be offset similarly to the original project.",  # noqa: E501
    ] = None,
    new_project_name: Annotated[str | None, "The name for the duplicated project in Asana."] = None,
    new_project_team_id: Annotated[
        str | None,
        "Globally unique identifier for the team to set for the new project. If not provided, the project will remain in the same team as the original.",  # noqa: E501
    ] = None,
    pretty_output_enabled: Annotated[
        bool | None,
        "Enable pretty formatting for the response output. Useful for debugging but increases response size and time.",  # noqa: E501
    ] = None,
    skip_weekends_in_schedule: Annotated[
        bool | None,
        "Set to true to skip weekends for auto-shifted dates in the duplicated project schedule. This is a required parameter.",  # noqa: E501
    ] = None,
    start_date_for_first_task: Annotated[
        str | None,
        "Sets the first start date in the duplicated project. Adjusts other start dates based on this.",  # noqa: E501
    ] = None,
) -> Annotated[dict[str, Any], "Response from the API endpoint 'duplicateProject'."]:
    """Initiate duplication of a project in Asana.

    Creates and returns a job to handle the asynchronous duplication of a project in Asana. It requires 'projects:write' scope."""  # noqa: E501
    request_data: Any = {
        "data": {
            "name": new_project_name,
            "team": new_project_team_id,
            "include": include_elements_in_project_duplication,
            "schedule_dates": {
                "should_skip_weekends": skip_weekends_in_schedule,
                "due_on": last_due_date_in_duplicated_project,
                "start_on": start_date_for_first_task,
            },
        }
    }
    if isinstance(request_data, dict):
        request_data = remove_none_values(request_data)
    content = json.dumps(request_data) if request_data else None
    response = await make_request(
        url="https://app.asana.com/api/1.0/projects/{project_gid}/duplicate".format(  # noqa: UP032
            project_gid=project_global_id
        ),
        method="POST",
        params=remove_none_values({
            "opt_pretty": pretty_output_enabled,
            "opt_fields": include_optional_fields,
        }),
        headers=remove_none_values({
            "Content-Type": "application/json",
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            ),
        }),
        content=content,
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(requires_auth=OAuth2(id="arcade-asana", scopes=["projects:read"]))
async def get_projects_for_task(
    context: ToolContext,
    task_identifier: Annotated[
        str, "The identifier of the task to retrieve associated projects for."
    ],
    enable_pretty_output: Annotated[
        bool | None,
        "Set to true to receive the output in a 'pretty' format, useful for debugging. Note: This increases response time and size.",  # noqa: E501
    ] = None,
    optional_fields_to_include: Annotated[
        list[str] | None,
        "Comma-separated list of optional project properties to include in the response.",
    ] = None,
    pagination_offset: Annotated[
        str | None,
        "Offset token used for pagination. If not provided, the first page is returned. Only pass an offset returned from a previous request.",  # noqa: E501
    ] = None,
    results_per_page: Annotated[
        int | None, "Number of project entries to return per page, between 1 and 100."
    ] = None,
) -> Annotated[dict[str, Any], "Response from the API endpoint 'getProjectsForTask'."]:
    """Retrieve all projects associated with a specific task.

    Returns a compact list of projects that a specified task is part of. Useful for understanding task context within various projects."""  # noqa: E501
    request_data: Any = None
    if isinstance(request_data, dict):
        request_data = remove_none_values(request_data)
    content = json.dumps(request_data) if request_data else None
    response = await make_request(
        url="https://app.asana.com/api/1.0/tasks/{task_gid}/projects".format(  # noqa: UP032
            task_gid=task_identifier
        ),
        method="GET",
        params=remove_none_values({
            "opt_pretty": enable_pretty_output,
            "limit": results_per_page,
            "offset": pagination_offset,
            "opt_fields": optional_fields_to_include,
        }),
        headers=remove_none_values({
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            )
        }),
        content=content,
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(requires_auth=OAuth2(id="arcade-asana", scopes=["projects:read"]))
async def get_team_projects(
    context: ToolContext,
    team_identifier: Annotated[
        str,
        "Globally unique identifier for the team whose projects are to be retrieved. It is required to specify the team.",  # noqa: E501
    ],
    enable_pretty_output: Annotated[
        bool | None,
        "Enable pretty formatting for the response, making it more readable. Use mainly for debugging due to increased response size.",  # noqa: E501
    ] = None,
    include_optional_properties: Annotated[
        list[str] | None,
        "A list of optional properties to include in the response for the projects. Specify as a list of strings.",  # noqa: E501
    ] = None,
    only_archived_projects: Annotated[
        bool | None,
        "Return projects based on their archived status. True for archived projects, False for non-archived projects.",  # noqa: E501
    ] = None,
    pagination_offset_token: Annotated[
        str | None,
        "Offset token used for pagination to fetch the next page of results. If not provided, the first page is returned. Use an offset from a previous response for subsequent pages.",  # noqa: E501
    ] = None,
    results_per_page: Annotated[
        int | None, "Number of projects to return per page, between 1 and 100."
    ] = None,
) -> Annotated[dict[str, Any], "Response from the API endpoint 'getProjectsForTeam'."]:
    """Fetch the list of projects for a specified team.

    This tool retrieves the compact project records for all projects within a specified team. It should be used when you need to access or display project details for a given team in Asana. Requires the 'projects:read' scope."""  # noqa: E501
    request_data: Any = None
    if isinstance(request_data, dict):
        request_data = remove_none_values(request_data)
    content = json.dumps(request_data) if request_data else None
    response = await make_request(
        url="https://app.asana.com/api/1.0/teams/{team_gid}/projects".format(  # noqa: UP032
            team_gid=team_identifier
        ),
        method="GET",
        params=remove_none_values({
            "opt_pretty": enable_pretty_output,
            "limit": results_per_page,
            "offset": pagination_offset_token,
            "archived": only_archived_projects,
            "opt_fields": include_optional_properties,
        }),
        headers=remove_none_values({
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            )
        }),
        content=content,
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(requires_auth=OAuth2(id="arcade-asana", scopes=["projects:write"]))
async def create_asana_project_for_team(
    context: ToolContext,
    mode: Annotated[
        ToolMode,
        "Operation mode: 'get_request_schema' returns the OpenAPI spec "
        "for the request body, 'execute' performs the actual operation",
    ],
    team_global_id: Annotated[
        str | None,
        "Globally unique identifier for the team in Asana.  Required when mode is 'execute', ignored when mode is 'get_request_schema'.",  # noqa: E501
    ] = None,
    include_optional_project_properties: Annotated[
        list[str] | None,
        "Specify properties to include as a comma-separated list to get additional project details.  Only used when mode is 'execute'.",  # noqa: E501
    ] = None,
    enable_pretty_output: Annotated[
        bool | None,
        "Set to true for readable output with line breaks and indentation. Recommended for debugging due to increased response size.  Only used when mode is 'execute'.",  # noqa: E501
    ] = None,
    request_body: Annotated[
        str | None,
        "Stringified JSON representing the request body. Required when "
        "mode is 'execute', ignored when mode is 'get_request_schema'",
    ] = None,
) -> Annotated[dict[str, Any], "Response from the API endpoint 'createProjectForTeam'."]:
    """Create a new Asana project for a specific team.

    This tool creates a new project within a specified team on Asana and returns the full record of the created project. It requires a 'projects:write' scope.

    Note: Understanding the request schema is necessary to properly create
    the stringified JSON input object for execution.\n\nThis operation also requires path, query parameters.

    Modes:
    - GET_REQUEST_SCHEMA: Returns the schema. Only call if you don't
      already have it. Do NOT call repeatedly if you already received
      the schema.
    - EXECUTE: Performs the operation with the provided request body
      JSON.\n      Note: You must also provide the required path, query parameters when executing.

    If you need the schema, call with mode='get_request_schema' ONCE, then execute.
    """  # noqa: E501
    if mode == ToolMode.GET_REQUEST_SCHEMA:
        return {
            "request_body_schema": REQUEST_BODY_SCHEMAS["CREATEASANAPROJECTFORTEAM"],
            "instructions": (
                "Use the request_body_schema to construct a valid JSON object. "
                "Once you have populated the object following the schema "
                "structure and requirements, call this tool again with "
                "mode='execute' and the stringified JSON as the "
                "request_body parameter along with the required path, query parameters. "
                "Do NOT call the schema mode again - you already have "
                "the schema now."
            ),
        }

    # Mode is EXECUTE - validate parameters
    # Validate required parameters
    missing_params = []
    if not team_global_id:
        missing_params.append(("team_global_id", "path"))

    if missing_params:
        param_names = [p[0] for p in missing_params]
        param_details = ", ".join([f"{p[0]} ({p[1]})" for p in missing_params])
        raise RetryableToolError(
            message=f"Missing required parameters: {param_names}",
            developer_message=(f"Required parameters validation failed: {param_details}"),
            additional_prompt_content=(
                f"The following required parameters are missing: "
                f"{param_details}. Please call this tool again with all "
                "required parameters."
            ),
        )

    # Validate request body is provided (not None or empty string)
    # Note: Empty objects like {} are allowed - schema validation will check if valid
    if request_body is None or request_body.strip() == "":
        raise RetryableToolError(
            message="Request body is required when mode is 'execute'",
            developer_message="The request_body parameter was null or empty string",
            additional_prompt_content=(
                "The request body is required to perform this operation. "
                "Use the schema below to construct a valid JSON object, "
                "then call this tool again in execute mode with the "
                "stringified JSON as the request_body parameter.\n\n"
                "Schema:\n\n```json\n" + REQUEST_BODY_SCHEMAS["CREATEASANAPROJECTFORTEAM"] + "\n```"
            ),
        )

    # Parse JSON
    try:
        request_data = json.loads(request_body)
    except json.JSONDecodeError as e:
        raise RetryableToolError(
            message=f"Invalid JSON in request body: {e!s}",
            developer_message=f"JSON parsing failed: {e!s}",
            additional_prompt_content=(
                f"The request body contains invalid JSON. Error: {e!s}\n\n"
                "Please provide a valid JSON string that matches the schema "
                "below, then call this tool again in execute mode.\n\n"
                "Schema:\n\n```json\n" + REQUEST_BODY_SCHEMAS["CREATEASANAPROJECTFORTEAM"] + "\n```"
            ),
        ) from e

    response = await make_request_with_schema_validation(
        url="https://app.asana.com/api/1.0/teams/{team_gid}/projects".format(  # noqa: UP032
            team_gid=team_global_id
        ),
        method="POST",
        request_data=request_data,
        schema=REQUEST_BODY_SCHEMAS["CREATEASANAPROJECTFORTEAM"],
        params=remove_none_values({
            "opt_pretty": enable_pretty_output,
            "opt_fields": include_optional_project_properties,
        }),
        headers=remove_none_values({
            "Content-Type": "application/json",
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            ),
        }),
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(requires_auth=OAuth2(id="arcade-asana", scopes=["projects:read"]))
async def get_workspace_projects(
    context: ToolContext,
    workspace_identifier: Annotated[
        str, "Globally unique identifier for the workspace or organization in Asana."
    ],
    archived_projects_only: Annotated[
        bool | None,
        "Specify `true` to return only archived projects, and `false` to include unarchived ones.",
    ] = None,
    enable_pretty_output: Annotated[
        bool | None,
        "Enable pretty formatting for the response output. Use this for a readable JSON format, mainly during debugging.",  # noqa: E501
    ] = None,
    optional_fields_to_include: Annotated[
        list[str] | None,
        "Specify a list of property names to include in the response. These properties are not included by default.",  # noqa: E501
    ] = None,
    pagination_offset_token: Annotated[
        str | None,
        "Offset token for pagination. Use to request subsequent pages. If omitted, returns the first page. Only use tokens from previous requests.",  # noqa: E501
    ] = None,
    results_per_page: Annotated[
        int | None,
        "Specify the number of project records to return per page. Must be between 1 and 100.",
    ] = None,
) -> Annotated[dict[str, Any], "Response from the API endpoint 'getProjectsForWorkspace'."]:
    """Fetch compact project records for a workspace.

    This tool retrieves the compact project records for all projects in a specified workspace from Asana. It should be called when you need to access project information within a workspace. Note: May timeout for large domains; consider using the `/teams/{team_gid}/projects` endpoint for efficiency."""  # noqa: E501
    request_data: Any = None
    if isinstance(request_data, dict):
        request_data = remove_none_values(request_data)
    content = json.dumps(request_data) if request_data else None
    response = await make_request(
        url="https://app.asana.com/api/1.0/workspaces/{workspace_gid}/projects".format(  # noqa: UP032
            workspace_gid=workspace_identifier
        ),
        method="GET",
        params=remove_none_values({
            "opt_pretty": enable_pretty_output,
            "limit": results_per_page,
            "offset": pagination_offset_token,
            "archived": archived_projects_only,
            "opt_fields": optional_fields_to_include,
        }),
        headers=remove_none_values({
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            )
        }),
        content=content,
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(requires_auth=OAuth2(id="arcade-asana", scopes=["projects:write"]))
async def create_project_in_workspace(
    context: ToolContext,
    mode: Annotated[
        ToolMode,
        "Operation mode: 'get_request_schema' returns the OpenAPI spec "
        "for the request body, 'execute' performs the actual operation",
    ],
    workspace_identifier: Annotated[
        str | None,
        "Globally unique identifier for the workspace or organization where the project will be created.  Required when mode is 'execute', ignored when mode is 'get_request_schema'.",  # noqa: E501
    ] = None,
    include_optional_properties: Annotated[
        list[str] | None,
        "Comma-separated list of optional properties to include in the response.  Only used when mode is 'execute'.",  # noqa: E501
    ] = None,
    pretty_output: Annotated[
        bool | None,
        "Set to true for formatted, readable output. Use mainly during debugging as it increases response time and size.  Only used when mode is 'execute'.",  # noqa: E501
    ] = None,
    request_body: Annotated[
        str | None,
        "Stringified JSON representing the request body. Required when "
        "mode is 'execute', ignored when mode is 'get_request_schema'",
    ] = None,
) -> Annotated[dict[str, Any], "Response from the API endpoint 'createProjectForWorkspace'."]:
    """Create a new project in a specified workspace.

    Use this tool to create a new project in a given workspace on Asana. If the workspace is an organization, provide a team to share the project. This action requires the 'projects:write' scope. Returns the full details of the new project.

    Note: Understanding the request schema is necessary to properly create
    the stringified JSON input object for execution.\n\nThis operation also requires path, query parameters.

    Modes:
    - GET_REQUEST_SCHEMA: Returns the schema. Only call if you don't
      already have it. Do NOT call repeatedly if you already received
      the schema.
    - EXECUTE: Performs the operation with the provided request body
      JSON.\n      Note: You must also provide the required path, query parameters when executing.

    If you need the schema, call with mode='get_request_schema' ONCE, then execute.
    """  # noqa: E501
    if mode == ToolMode.GET_REQUEST_SCHEMA:
        return {
            "request_body_schema": REQUEST_BODY_SCHEMAS["CREATEPROJECTINWORKSPACE"],
            "instructions": (
                "Use the request_body_schema to construct a valid JSON object. "
                "Once you have populated the object following the schema "
                "structure and requirements, call this tool again with "
                "mode='execute' and the stringified JSON as the "
                "request_body parameter along with the required path, query parameters. "
                "Do NOT call the schema mode again - you already have "
                "the schema now."
            ),
        }

    # Mode is EXECUTE - validate parameters
    # Validate required parameters
    missing_params = []
    if not workspace_identifier:
        missing_params.append(("workspace_identifier", "path"))

    if missing_params:
        param_names = [p[0] for p in missing_params]
        param_details = ", ".join([f"{p[0]} ({p[1]})" for p in missing_params])
        raise RetryableToolError(
            message=f"Missing required parameters: {param_names}",
            developer_message=(f"Required parameters validation failed: {param_details}"),
            additional_prompt_content=(
                f"The following required parameters are missing: "
                f"{param_details}. Please call this tool again with all "
                "required parameters."
            ),
        )

    # Validate request body is provided (not None or empty string)
    # Note: Empty objects like {} are allowed - schema validation will check if valid
    if request_body is None or request_body.strip() == "":
        raise RetryableToolError(
            message="Request body is required when mode is 'execute'",
            developer_message="The request_body parameter was null or empty string",
            additional_prompt_content=(
                "The request body is required to perform this operation. "
                "Use the schema below to construct a valid JSON object, "
                "then call this tool again in execute mode with the "
                "stringified JSON as the request_body parameter.\n\n"
                "Schema:\n\n```json\n" + REQUEST_BODY_SCHEMAS["CREATEPROJECTINWORKSPACE"] + "\n```"
            ),
        )

    # Parse JSON
    try:
        request_data = json.loads(request_body)
    except json.JSONDecodeError as e:
        raise RetryableToolError(
            message=f"Invalid JSON in request body: {e!s}",
            developer_message=f"JSON parsing failed: {e!s}",
            additional_prompt_content=(
                f"The request body contains invalid JSON. Error: {e!s}\n\n"
                "Please provide a valid JSON string that matches the schema "
                "below, then call this tool again in execute mode.\n\n"
                "Schema:\n\n```json\n" + REQUEST_BODY_SCHEMAS["CREATEPROJECTINWORKSPACE"] + "\n```"
            ),
        ) from e

    response = await make_request_with_schema_validation(
        url="https://app.asana.com/api/1.0/workspaces/{workspace_gid}/projects".format(  # noqa: UP032
            workspace_gid=workspace_identifier
        ),
        method="POST",
        request_data=request_data,
        schema=REQUEST_BODY_SCHEMAS["CREATEPROJECTINWORKSPACE"],
        params=remove_none_values({
            "opt_pretty": pretty_output,
            "opt_fields": include_optional_properties,
        }),
        headers=remove_none_values({
            "Content-Type": "application/json",
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            ),
        }),
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(requires_auth=OAuth2(id="arcade-asana", scopes=["projects:write"]))
async def add_custom_field_setting_to_project(
    context: ToolContext,
    mode: Annotated[
        ToolMode,
        "Operation mode: 'get_request_schema' returns the OpenAPI spec "
        "for the request body, 'execute' performs the actual operation",
    ],
    project_global_id: Annotated[
        str | None,
        "Globally unique identifier for the project in Asana.  Required when mode is 'execute', ignored when mode is 'get_request_schema'.",  # noqa: E501
    ] = None,
    include_optional_properties: Annotated[
        list[str] | None,
        "Comma-separated list of properties to include in the response, typically those excluded by default.  Only used when mode is 'execute'.",  # noqa: E501
    ] = None,
    enable_pretty_output: Annotated[
        bool | None,
        "Set to true for a readable output format with line breaks and indentation. Increases response time and size, so use it mainly for debugging.  Only used when mode is 'execute'.",  # noqa: E501
    ] = None,
    request_body: Annotated[
        str | None,
        "Stringified JSON representing the request body. Required when "
        "mode is 'execute', ignored when mode is 'get_request_schema'",
    ] = None,
) -> Annotated[dict[str, Any], "Response from the API endpoint 'addCustomFieldSettingForProject'."]:
    """Add a custom field setting to a project in Asana.

    Use this tool to associate a custom field with a specific project in Asana. This operation requires the 'projects:write' scope.

    Note: Understanding the request schema is necessary to properly create
    the stringified JSON input object for execution.\n\nThis operation also requires path, query parameters.

    Modes:
    - GET_REQUEST_SCHEMA: Returns the schema. Only call if you don't
      already have it. Do NOT call repeatedly if you already received
      the schema.
    - EXECUTE: Performs the operation with the provided request body
      JSON.\n      Note: You must also provide the required path, query parameters when executing.

    If you need the schema, call with mode='get_request_schema' ONCE, then execute.
    """  # noqa: E501
    if mode == ToolMode.GET_REQUEST_SCHEMA:
        return {
            "request_body_schema": REQUEST_BODY_SCHEMAS["ADDCUSTOMFIELDSETTINGTOPROJECT"],
            "instructions": (
                "Use the request_body_schema to construct a valid JSON object. "
                "Once you have populated the object following the schema "
                "structure and requirements, call this tool again with "
                "mode='execute' and the stringified JSON as the "
                "request_body parameter along with the required path, query parameters. "
                "Do NOT call the schema mode again - you already have "
                "the schema now."
            ),
        }

    # Mode is EXECUTE - validate parameters
    # Validate required parameters
    missing_params = []
    if not project_global_id:
        missing_params.append(("project_global_id", "path"))

    if missing_params:
        param_names = [p[0] for p in missing_params]
        param_details = ", ".join([f"{p[0]} ({p[1]})" for p in missing_params])
        raise RetryableToolError(
            message=f"Missing required parameters: {param_names}",
            developer_message=(f"Required parameters validation failed: {param_details}"),
            additional_prompt_content=(
                f"The following required parameters are missing: "
                f"{param_details}. Please call this tool again with all "
                "required parameters."
            ),
        )

    # Validate request body is provided (not None or empty string)
    # Note: Empty objects like {} are allowed - schema validation will check if valid
    if request_body is None or request_body.strip() == "":
        raise RetryableToolError(
            message="Request body is required when mode is 'execute'",
            developer_message="The request_body parameter was null or empty string",
            additional_prompt_content=(
                "The request body is required to perform this operation. "
                "Use the schema below to construct a valid JSON object, "
                "then call this tool again in execute mode with the "
                "stringified JSON as the request_body parameter.\n\n"
                "Schema:\n\n```json\n"
                + REQUEST_BODY_SCHEMAS["ADDCUSTOMFIELDSETTINGTOPROJECT"]
                + "\n```"
            ),
        )

    # Parse JSON
    try:
        request_data = json.loads(request_body)
    except json.JSONDecodeError as e:
        raise RetryableToolError(
            message=f"Invalid JSON in request body: {e!s}",
            developer_message=f"JSON parsing failed: {e!s}",
            additional_prompt_content=(
                f"The request body contains invalid JSON. Error: {e!s}\n\n"
                "Please provide a valid JSON string that matches the schema "
                "below, then call this tool again in execute mode.\n\n"
                "Schema:\n\n```json\n"
                + REQUEST_BODY_SCHEMAS["ADDCUSTOMFIELDSETTINGTOPROJECT"]
                + "\n```"
            ),
        ) from e

    response = await make_request_with_schema_validation(
        url="https://app.asana.com/api/1.0/projects/{project_gid}/addCustomFieldSetting".format(  # noqa: UP032
            project_gid=project_global_id
        ),
        method="POST",
        request_data=request_data,
        schema=REQUEST_BODY_SCHEMAS["ADDCUSTOMFIELDSETTINGTOPROJECT"],
        params=remove_none_values({
            "opt_pretty": enable_pretty_output,
            "opt_fields": include_optional_properties,
        }),
        headers=remove_none_values({
            "Content-Type": "application/json",
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            ),
        }),
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(requires_auth=OAuth2(id="arcade-asana", scopes=["projects:write"]))
async def remove_custom_field_from_project(
    context: ToolContext,
    project_unique_id: Annotated[str, "Globally unique identifier for the Asana project."],
    custom_field_id_to_remove: Annotated[
        str | None, "The ID of the custom field to remove from the specified project in Asana."
    ] = None,
    enable_pretty_output: Annotated[
        bool | None,
        "Formats the response in a readable way with line breaks and indentation. Use for debugging as it increases response time and size.",  # noqa: E501
    ] = None,
) -> Annotated[
    dict[str, Any], "Response from the API endpoint 'removeCustomFieldSettingForProject'."
]:
    """Remove a custom field setting from an Asana project.

    Use this tool to remove a custom field setting from a specified project in Asana. This requires the 'projects:write' scope."""  # noqa: E501
    request_data: Any = {"data": {"custom_field": custom_field_id_to_remove}}
    if isinstance(request_data, dict):
        request_data = remove_none_values(request_data)
    content = json.dumps(request_data) if request_data else None
    response = await make_request(
        url="https://app.asana.com/api/1.0/projects/{project_gid}/removeCustomFieldSetting".format(  # noqa: UP032
            project_gid=project_unique_id
        ),
        method="POST",
        params=remove_none_values({"opt_pretty": enable_pretty_output}),
        headers=remove_none_values({
            "Content-Type": "application/json",
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            ),
        }),
        content=content,
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(requires_auth=OAuth2(id="arcade-asana", scopes=["projects:read"]))
async def get_task_counts_for_project(
    context: ToolContext,
    project_global_id: Annotated[str, "Globally unique identifier for the project in Asana."],
    enable_pretty_output: Annotated[
        bool | None,
        "Set to true to format the response in a readable way with line breaks and indentation. Use for debugging due to increased response size.",  # noqa: E501
    ] = None,
    include_optional_fields: Annotated[
        list[str] | None,
        "Comma-separated list of properties to include in the response. Opt-in to get specific data fields from the endpoint.",  # noqa: E501
    ] = None,
) -> Annotated[dict[str, Any], "Response from the API endpoint 'getTaskCountsForProject'."]:
    """Retrieve task count details for a specific project in Asana.

    Use this tool to get detailed counts of tasks, including total, incomplete, and completed tasks, for a specific project in Asana. Ensure the necessary scopes are granted: `projects:read`, and optionally `custom_fields:read` and `teams:read`. Opt in to specific fields to retrieve data, considering rate and cost limits."""  # noqa: E501
    request_data: Any = None
    if isinstance(request_data, dict):
        request_data = remove_none_values(request_data)
    content = json.dumps(request_data) if request_data else None
    response = await make_request(
        url="https://app.asana.com/api/1.0/projects/{project_gid}/task_counts".format(  # noqa: UP032
            project_gid=project_global_id
        ),
        method="GET",
        params=remove_none_values({
            "opt_pretty": enable_pretty_output,
            "opt_fields": include_optional_fields,
        }),
        headers=remove_none_values({
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            )
        }),
        content=content,
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(requires_auth=OAuth2(id="arcade-asana"))
async def add_members_to_project(
    context: ToolContext,
    project_unique_identifier: Annotated[
        str, "Globally unique identifier for the project in Asana."
    ],
    include_optional_properties: Annotated[
        list[str] | None,
        "A comma-separated list of optional properties to include in the response. This adds excluded properties by default.",  # noqa: E501
    ] = None,
    pretty_output: Annotated[
        bool | None,
        "If true, the response is formatted with line breaks and indentation for readability. Use mainly for debugging.",  # noqa: E501
    ] = None,
    user_identifiers_array: Annotated[
        str | None,
        "An array of strings identifying users to be added to the project. Values can be 'me', an email, or a user GID.",  # noqa: E501
    ] = None,
) -> Annotated[dict[str, Any], "Response from the API endpoint 'addMembersForProject'."]:
    """Add specified users as members of a project in Asana.

    Use this tool to add users as members of a specific project. This may also add users as followers depending on their notification settings."""  # noqa: E501
    request_data: Any = {"data": {"members": user_identifiers_array}}
    if isinstance(request_data, dict):
        request_data = remove_none_values(request_data)
    content = json.dumps(request_data) if request_data else None
    response = await make_request(
        url="https://app.asana.com/api/1.0/projects/{project_gid}/addMembers".format(  # noqa: UP032
            project_gid=project_unique_identifier
        ),
        method="POST",
        params=remove_none_values({
            "opt_pretty": pretty_output,
            "opt_fields": include_optional_properties,
        }),
        headers=remove_none_values({
            "Content-Type": "application/json",
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            ),
        }),
        content=content,
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(requires_auth=OAuth2(id="arcade-asana"))
async def remove_members_from_project(
    context: ToolContext,
    project_identifier: Annotated[str, "Globally unique identifier for the project."],
    enable_pretty_output: Annotated[
        bool | None,
        "Set to true for formatted, human-readable JSON. Increases response size; use for debugging.",  # noqa: E501
    ] = None,
    include_optional_fields: Annotated[
        list[str] | None,
        "A list of optional fields to include in the response. Use a comma-separated list to specify.",  # noqa: E501
    ] = None,
    user_identifiers: Annotated[
        str | None,
        'Array of user identifiers to be removed from the project. Accepts "me", email, or user gid.',  # noqa: E501
    ] = None,
) -> Annotated[dict[str, Any], "Response from the API endpoint 'removeMembersForProject'."]:
    """Remove specified users from a project in Asana.

    This tool calls the Asana API to remove a list of specified users from the members of a project. It returns the updated project record after the removal operation."""  # noqa: E501
    request_data: Any = {"data": {"members": user_identifiers}}
    if isinstance(request_data, dict):
        request_data = remove_none_values(request_data)
    content = json.dumps(request_data) if request_data else None
    response = await make_request(
        url="https://app.asana.com/api/1.0/projects/{project_gid}/removeMembers".format(  # noqa: UP032
            project_gid=project_identifier
        ),
        method="POST",
        params=remove_none_values({
            "opt_pretty": enable_pretty_output,
            "opt_fields": include_optional_fields,
        }),
        headers=remove_none_values({
            "Content-Type": "application/json",
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            ),
        }),
        content=content,
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(requires_auth=OAuth2(id="arcade-asana"))
async def add_followers_to_project(
    context: ToolContext,
    project_unique_id: Annotated[
        str, "Globally unique identifier for the project to which followers will be added."
    ],
    enable_pretty_output: Annotated[
        bool | None,
        "Enable pretty JSON output for readability, useful for debugging. May slow responses.",
    ] = None,
    optional_fields_to_include: Annotated[
        list[str] | None,
        "Comma-separated list of optional properties to include in the response to enrich data output.",  # noqa: E501
    ] = None,
    user_identifiers_to_add_as_followers: Annotated[
        str | None,
        "An array of user identifiers to add as followers. Accepts 'me', email addresses, or user gids.",  # noqa: E501
    ] = None,
) -> Annotated[dict[str, Any], "Response from the API endpoint 'addFollowersForProject'."]:
    """Add specified users as followers to an Asana project.

    This tool adds a list of users as followers to a specified project in Asana. Followers receive \"tasks added\" notifications and are made members of the project if they aren't already. Use this when needing to update project followers."""  # noqa: E501
    request_data: Any = {"data": {"followers": user_identifiers_to_add_as_followers}}
    if isinstance(request_data, dict):
        request_data = remove_none_values(request_data)
    content = json.dumps(request_data) if request_data else None
    response = await make_request(
        url="https://app.asana.com/api/1.0/projects/{project_gid}/addFollowers".format(  # noqa: UP032
            project_gid=project_unique_id
        ),
        method="POST",
        params=remove_none_values({
            "opt_pretty": enable_pretty_output,
            "opt_fields": optional_fields_to_include,
        }),
        headers=remove_none_values({
            "Content-Type": "application/json",
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            ),
        }),
        content=content,
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(requires_auth=OAuth2(id="arcade-asana"))
async def remove_followers_from_project(
    context: ToolContext,
    project_global_id: Annotated[str, "Globally unique identifier for the project in Asana."],
    enable_pretty_output: Annotated[
        bool | None,
        "Set to true to format the response with line breaks and indentation for readability. Useful for debugging.",  # noqa: E501
    ] = None,
    include_optional_properties: Annotated[
        list[str] | None,
        "A list of property names to include in the response, specified as strings. This should be used to include properties that are not returned by default.",  # noqa: E501
    ] = None,
    user_identifiers_to_unfollow: Annotated[
        str | None,
        'Array of user identifiers (e.g. "me", email, or gid) to remove from following the project.',  # noqa: E501
    ] = None,
) -> Annotated[dict[str, Any], "Response from the API endpoint 'removeFollowersForProject'."]:
    """Remove specified users from following a project in Asana.

    This tool removes a list of specified users from following a project in Asana without affecting their membership status. It should be called when you want to update who follows a project without altering project membership."""  # noqa: E501
    request_data: Any = {"data": {"followers": user_identifiers_to_unfollow}}
    if isinstance(request_data, dict):
        request_data = remove_none_values(request_data)
    content = json.dumps(request_data) if request_data else None
    response = await make_request(
        url="https://app.asana.com/api/1.0/projects/{project_gid}/removeFollowers".format(  # noqa: UP032
            project_gid=project_global_id
        ),
        method="POST",
        params=remove_none_values({
            "opt_pretty": enable_pretty_output,
            "opt_fields": include_optional_properties,
        }),
        headers=remove_none_values({
            "Content-Type": "application/json",
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            ),
        }),
        content=content,
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(requires_auth=OAuth2(id="arcade-asana"))
async def create_project_template(
    context: ToolContext,
    project_global_id: Annotated[
        str,
        "Globally unique identifier for the project. Required to specify which project is being saved as a template.",  # noqa: E501
    ],
    enable_pretty_output: Annotated[
        bool | None,
        "Enable pretty output formatting for easier readability in the response. Use mainly for debugging as it may increase response time and size.",  # noqa: E501
    ] = None,
    include_optional_properties: Annotated[
        list[str] | None,
        "Specify a list of optional properties to include in the response by listing their names as strings.",  # noqa: E501
    ] = None,
    make_template_public: Annotated[
        bool | None,
        "Specify true to make the project template public to its team. Use false to keep it private.",  # noqa: E501
    ] = None,
    team_id_for_project_template: Annotated[
        str | None,
        "Specify the team ID for the new project template. Use this when the project is in an organization.",  # noqa: E501
    ] = None,
    template_name: Annotated[str | None, "The name of the new project template in Asana."] = None,
    workspace_id_for_project_template: Annotated[
        str | None,
        "Globally unique identifier for the workspace of the new project template. Use only if the project is in a workspace.",  # noqa: E501
    ] = None,
) -> Annotated[dict[str, Any], "Response from the API endpoint 'projectSaveAsTemplate'."]:
    """Create a project template in Asana asynchronously.

    This tool triggers the creation of a project template in Asana and returns details of the asynchronous job handling the process. Use this to save an existing project as a template, enabling its reuse."""  # noqa: E501
    request_data: Any = {
        "data": {
            "name": template_name,
            "team": team_id_for_project_template,
            "workspace": workspace_id_for_project_template,
            "public": make_template_public,
        }
    }
    if isinstance(request_data, dict):
        request_data = remove_none_values(request_data)
    content = json.dumps(request_data) if request_data else None
    response = await make_request(
        url="https://app.asana.com/api/1.0/projects/{project_gid}/saveAsTemplate".format(  # noqa: UP032
            project_gid=project_global_id
        ),
        method="POST",
        params=remove_none_values({
            "opt_pretty": enable_pretty_output,
            "opt_fields": include_optional_properties,
        }),
        headers=remove_none_values({
            "Content-Type": "application/json",
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            ),
        }),
        content=content,
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(requires_auth=OAuth2(id="arcade-asana"))
async def fetch_reactions_by_emoji(
    context: ToolContext,
    emoji_base_character: Annotated[
        str,
        "Specify the emoji base character to filter reactions. Returns only reactions with this emoji.",  # noqa: E501
    ],
    object_gid: Annotated[
        str,
        "Globally unique identifier (GID) for the Asana object (status update or story) from which to fetch reactions.",  # noqa: E501
    ],
    enable_pretty_output: Annotated[
        bool | None,
        "Enable for readable JSON output with line breaks and indentation. Use primarily for debugging due to increased response size and time.",  # noqa: E501
    ] = None,
    pagination_offset_token: Annotated[
        str | None,
        "Offset token to retrieve the next page of results. Use the token provided by a previous paginated request. If not specified, the API returns the first page.",  # noqa: E501
    ] = None,
    results_per_page: Annotated[
        int | None, "Specify the number of results to return per page. Must be between 1 and 100."
    ] = None,
) -> Annotated[dict[str, Any], "Response from the API endpoint 'getReactionsOnObject'."]:
    """Retrieve reactions with a specific emoji on an object.

    Use this tool to get a list of reactions for a specific emoji character on an Asana object. Useful for analyzing engagement or feedback expressed through emojis."""  # noqa: E501
    request_data: Any = None
    if isinstance(request_data, dict):
        request_data = remove_none_values(request_data)
    content = json.dumps(request_data) if request_data else None
    response = await make_request(
        url="https://app.asana.com/api/1.0/reactions",
        method="GET",
        params=remove_none_values({
            "opt_pretty": enable_pretty_output,
            "limit": results_per_page,
            "offset": pagination_offset_token,
            "target": object_gid,
            "emoji_base": emoji_base_character,
        }),
        headers=remove_none_values({
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            )
        }),
        content=content,
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(requires_auth=OAuth2(id="arcade-asana"))
async def trigger_asana_rule(
    context: ToolContext,
    mode: Annotated[
        ToolMode,
        "Operation mode: 'get_request_schema' returns the OpenAPI spec "
        "for the request body, 'execute' performs the actual operation",
    ],
    incoming_web_request_trigger_id: Annotated[
        str | None,
        "The ID of the incoming web request trigger used to execute the rule in Asana. This is automatically generated for the API endpoint.  Required when mode is 'execute', ignored when mode is 'get_request_schema'.",  # noqa: E501
    ] = None,
    request_body: Annotated[
        str | None,
        "Stringified JSON representing the request body. Required when "
        "mode is 'execute', ignored when mode is 'get_request_schema'",
    ] = None,
) -> Annotated[dict[str, Any], "Response from the API endpoint 'triggerRule'."]:
    """Trigger a rule in Asana using an incoming web request.

    Use this tool to trigger a specific rule in Asana that is set up with an \"incoming web request\" trigger. Ideal for automating workflows by executing preset rules on demand.

    Note: Understanding the request schema is necessary to properly create
    the stringified JSON input object for execution.\n\nThis operation also requires path parameters.

    Modes:
    - GET_REQUEST_SCHEMA: Returns the schema. Only call if you don't
      already have it. Do NOT call repeatedly if you already received
      the schema.
    - EXECUTE: Performs the operation with the provided request body
      JSON.\n      Note: You must also provide the required path parameters when executing.

    If you need the schema, call with mode='get_request_schema' ONCE, then execute.
    """  # noqa: E501
    if mode == ToolMode.GET_REQUEST_SCHEMA:
        return {
            "request_body_schema": REQUEST_BODY_SCHEMAS["TRIGGERASANARULE"],
            "instructions": (
                "Use the request_body_schema to construct a valid JSON object. "
                "Once you have populated the object following the schema "
                "structure and requirements, call this tool again with "
                "mode='execute' and the stringified JSON as the "
                "request_body parameter along with the required path parameters. "
                "Do NOT call the schema mode again - you already have "
                "the schema now."
            ),
        }

    # Mode is EXECUTE - validate parameters
    # Validate required parameters
    missing_params = []
    if not incoming_web_request_trigger_id:
        missing_params.append(("incoming_web_request_trigger_id", "path"))

    if missing_params:
        param_names = [p[0] for p in missing_params]
        param_details = ", ".join([f"{p[0]} ({p[1]})" for p in missing_params])
        raise RetryableToolError(
            message=f"Missing required parameters: {param_names}",
            developer_message=(f"Required parameters validation failed: {param_details}"),
            additional_prompt_content=(
                f"The following required parameters are missing: "
                f"{param_details}. Please call this tool again with all "
                "required parameters."
            ),
        )

    # Validate request body is provided (not None or empty string)
    # Note: Empty objects like {} are allowed - schema validation will check if valid
    if request_body is None or request_body.strip() == "":
        raise RetryableToolError(
            message="Request body is required when mode is 'execute'",
            developer_message="The request_body parameter was null or empty string",
            additional_prompt_content=(
                "The request body is required to perform this operation. "
                "Use the schema below to construct a valid JSON object, "
                "then call this tool again in execute mode with the "
                "stringified JSON as the request_body parameter.\n\n"
                "Schema:\n\n```json\n" + REQUEST_BODY_SCHEMAS["TRIGGERASANARULE"] + "\n```"
            ),
        )

    # Parse JSON
    try:
        request_data = json.loads(request_body)
    except json.JSONDecodeError as e:
        raise RetryableToolError(
            message=f"Invalid JSON in request body: {e!s}",
            developer_message=f"JSON parsing failed: {e!s}",
            additional_prompt_content=(
                f"The request body contains invalid JSON. Error: {e!s}\n\n"
                "Please provide a valid JSON string that matches the schema "
                "below, then call this tool again in execute mode.\n\n"
                "Schema:\n\n```json\n" + REQUEST_BODY_SCHEMAS["TRIGGERASANARULE"] + "\n```"
            ),
        ) from e

    response = await make_request_with_schema_validation(
        url="https://app.asana.com/api/1.0/rule_triggers/{rule_trigger_gid}/run".format(  # noqa: UP032
            rule_trigger_gid=incoming_web_request_trigger_id
        ),
        method="POST",
        request_data=request_data,
        schema=REQUEST_BODY_SCHEMAS["TRIGGERASANARULE"],
        params=remove_none_values({}),
        headers=remove_none_values({
            "Content-Type": "application/json",
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            ),
        }),
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(requires_auth=OAuth2(id="arcade-asana"))
async def get_asana_section(
    context: ToolContext,
    section_global_id: Annotated[
        str, "The globally unique identifier for the section to retrieve."
    ],
    enable_pretty_output: Annotated[
        bool | None,
        "Set to true to receive the response in a 'pretty' format with line breaks and indentation. Useful for debugging. This increases response time and size.",  # noqa: E501
    ] = None,
    include_optional_properties: Annotated[
        list[str] | None,
        "Specify a list of optional properties to include in the response. This should be an array of strings.",  # noqa: E501
    ] = None,
) -> Annotated[dict[str, Any], "Response from the API endpoint 'getSection'."]:
    """Retrieve a complete record of a single Asana section.

    Use this tool to obtain detailed information about a specific section in Asana by providing the section's GID."""  # noqa: E501
    request_data: Any = None
    if isinstance(request_data, dict):
        request_data = remove_none_values(request_data)
    content = json.dumps(request_data) if request_data else None
    response = await make_request(
        url="https://app.asana.com/api/1.0/sections/{section_gid}".format(  # noqa: UP032
            section_gid=section_global_id
        ),
        method="GET",
        params=remove_none_values({
            "opt_pretty": enable_pretty_output,
            "opt_fields": include_optional_properties,
        }),
        headers=remove_none_values({
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            )
        }),
        content=content,
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(requires_auth=OAuth2(id="arcade-asana"))
async def update_section_name_asana(
    context: ToolContext,
    section_global_identifier: Annotated[
        str, "The globally unique identifier for the section to be updated in Asana."
    ],
    include_optional_properties: Annotated[
        list[str] | None,
        "Comma-separated list of properties to include in the response. Allows inclusion of optional fields excluded by default.",  # noqa: E501
    ] = None,
    insert_after_section_id: Annotated[
        str | None,
        "Identifier of an existing section after which the updated section should be inserted. Cannot be used with `insert_before_section_id`.",  # noqa: E501
    ] = None,
    pretty_output: Annotated[
        bool | None,
        "Set to true for a readable, pretty formatted response with line breaks and indentation. Recommended for debugging as it increases response size.",  # noqa: E501
    ] = None,
    section_insert_before_id: Annotated[
        str | None,
        "The ID of an existing section in the project before which the updated section should be inserted. Cannot be provided with section_insert_after_id.",  # noqa: E501
    ] = None,
    section_name: Annotated[
        str | None, "The new name for the section. It cannot be an empty string."
    ] = None,
) -> Annotated[dict[str, Any], "Response from the API endpoint 'updateSection'."]:
    """Update the name of a specific section in Asana.

    Use this tool to update the name of an existing section in an Asana project. It specifically changes the `name` field of a section without affecting other fields. Ideal for modifying section titles while preserving other data."""  # noqa: E501
    request_data: Any = {
        "data": {
            "name": section_name,
            "insert_before": section_insert_before_id,
            "insert_after": insert_after_section_id,
        }
    }
    if isinstance(request_data, dict):
        request_data = remove_none_values(request_data)
    content = json.dumps(request_data) if request_data else None
    response = await make_request(
        url="https://app.asana.com/api/1.0/sections/{section_gid}".format(  # noqa: UP032
            section_gid=section_global_identifier
        ),
        method="PUT",
        params=remove_none_values({
            "opt_pretty": pretty_output,
            "opt_fields": include_optional_properties,
        }),
        headers=remove_none_values({
            "Content-Type": "application/json",
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            ),
        }),
        content=content,
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(requires_auth=OAuth2(id="arcade-asana"))
async def delete_asana_section(
    context: ToolContext,
    section_global_identifier: Annotated[
        str, "The globally unique identifier for the section to be deleted in Asana."
    ],
    enable_pretty_output: Annotated[
        bool | None,
        "If true, returns JSON with line breaks and indentation for readability. Increases response time and size, suitable for debugging.",  # noqa: E501
    ] = None,
) -> Annotated[dict[str, Any], "Response from the API endpoint 'deleteSection'."]:
    """Delete a specific, existing section in Asana.

    This tool deletes a specific, existing section in an Asana project using its GID. Note that the section must be empty to be deleted, and the last remaining section cannot be removed. The tool confirms deletion or provides error details if it fails."""  # noqa: E501
    request_data: Any = None
    if isinstance(request_data, dict):
        request_data = remove_none_values(request_data)
    content = json.dumps(request_data) if request_data else None
    response = await make_request(
        url="https://app.asana.com/api/1.0/sections/{section_gid}".format(  # noqa: UP032
            section_gid=section_global_identifier
        ),
        method="DELETE",
        params=remove_none_values({"opt_pretty": enable_pretty_output}),
        headers=remove_none_values({
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            )
        }),
        content=content,
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(requires_auth=OAuth2(id="arcade-asana"))
async def get_project_sections(
    context: ToolContext,
    project_unique_identifier: Annotated[
        str, "Globally unique identifier for the project in Asana."
    ],
    enable_pretty_output: Annotated[
        bool | None,
        "Enables readable formatting of the response when true, using line breaks and indentation. Recommended for debugging as it increases response time and size.",  # noqa: E501
    ] = None,
    included_optional_fields: Annotated[
        list[str] | None,
        "List of optional properties to include in the response, specified as strings. This allows you to fetch additional details about the sections that are not included by default.",  # noqa: E501
    ] = None,
    pagination_offset_token: Annotated[
        str | None,
        "Offset token for paginated API requests. Use it to retrieve the next set of results. If not provided, the API returns the first page.",  # noqa: E501
    ] = None,
    results_per_page: Annotated[
        int | None, "Specify the number of sections to return per page, between 1 and 100."
    ] = None,
) -> Annotated[dict[str, Any], "Response from the API endpoint 'getSectionsForProject'."]:
    """Fetch compact records for sections in a specified project.

    Use this tool to obtain a list of all sections within a specific project on Asana. It helps in managing and organizing project components effectively by retrieving key details of each section."""  # noqa: E501
    request_data: Any = None
    if isinstance(request_data, dict):
        request_data = remove_none_values(request_data)
    content = json.dumps(request_data) if request_data else None
    response = await make_request(
        url="https://app.asana.com/api/1.0/projects/{project_gid}/sections".format(  # noqa: UP032
            project_gid=project_unique_identifier
        ),
        method="GET",
        params=remove_none_values({
            "opt_pretty": enable_pretty_output,
            "limit": results_per_page,
            "offset": pagination_offset_token,
            "opt_fields": included_optional_fields,
        }),
        headers=remove_none_values({
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            )
        }),
        content=content,
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(requires_auth=OAuth2(id="arcade-asana"))
async def create_section_in_project(
    context: ToolContext,
    project_global_id: Annotated[
        str, "Globally unique identifier for the project in which to create the section."
    ],
    enable_pretty_output: Annotated[
        bool | None,
        "Set to true for a readable response format with line breaks and indentation. Use for debugging only.",  # noqa: E501
    ] = None,
    include_optional_properties: Annotated[
        list[str] | None, "Comma-separated list of optional properties to include in the response."
    ] = None,
    insert_after_section_id: Annotated[
        str | None,
        "ID of an existing section to insert the new section after. Cannot be used with insert_before_section_id.",  # noqa: E501
    ] = None,
    insert_before_section_id: Annotated[
        str | None,
        "ID of an existing section in the project before which the new section will be inserted. Cannot be used with 'insert_after_section_id'.",  # noqa: E501
    ] = None,
    section_name: Annotated[
        str | None, "The name to display as the section title in the project. This cannot be empty."
    ] = None,
) -> Annotated[dict[str, Any], "Response from the API endpoint 'createSectionForProject'."]:
    """Create a new section in an Asana project.

    Use this tool to create a new section within a specified Asana project. It returns the complete details of the newly added section, helping you organize tasks by sections."""  # noqa: E501
    request_data: Any = {
        "data": {
            "name": section_name,
            "insert_before": insert_before_section_id,
            "insert_after": insert_after_section_id,
        }
    }
    if isinstance(request_data, dict):
        request_data = remove_none_values(request_data)
    content = json.dumps(request_data) if request_data else None
    response = await make_request(
        url="https://app.asana.com/api/1.0/projects/{project_gid}/sections".format(  # noqa: UP032
            project_gid=project_global_id
        ),
        method="POST",
        params=remove_none_values({
            "opt_pretty": enable_pretty_output,
            "opt_fields": include_optional_properties,
        }),
        headers=remove_none_values({
            "Content-Type": "application/json",
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            ),
        }),
        content=content,
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(requires_auth=OAuth2(id="arcade-asana", scopes=["tasks:write"]))
async def add_task_to_section(
    context: ToolContext,
    section_global_identifier: Annotated[
        str, "The globally unique identifier for the section where the task will be added."
    ],
    enable_pretty_output: Annotated[
        bool | None,
        "Enable pretty formatting for the API response, with line breaks and indentation for readability. Use mainly for debugging.",  # noqa: E501
    ] = None,
    insert_after_task_id: Annotated[
        str | None,
        "Specify the task ID after which the new task should be inserted within the section. Cannot be used with insert_before.",  # noqa: E501
    ] = None,
    insert_task_before: Annotated[
        str | None,
        "The ID of an existing task in the section to insert the new task before. Cannot be used with insert_task_after.",  # noqa: E501
    ] = None,
    task_description: Annotated[
        str | None, "The name or description of the task to be added to the specified section."
    ] = None,
) -> Annotated[dict[str, Any], "Response from the API endpoint 'addTaskForSection'."]:
    """Add a task to a specified section in Asana.

    Use this tool to add a task to a specific, existing section in an Asana project. The task will be positioned at the top unless specified otherwise, and will be removed from other sections in the project. It cannot be used to add separators."""  # noqa: E501
    request_data: Any = {
        "data": {
            "task": task_description,
            "insert_before": insert_task_before,
            "insert_after": insert_after_task_id,
        }
    }
    if isinstance(request_data, dict):
        request_data = remove_none_values(request_data)
    content = json.dumps(request_data) if request_data else None
    response = await make_request(
        url="https://app.asana.com/api/1.0/sections/{section_gid}/addTask".format(  # noqa: UP032
            section_gid=section_global_identifier
        ),
        method="POST",
        params=remove_none_values({"opt_pretty": enable_pretty_output}),
        headers=remove_none_values({
            "Content-Type": "application/json",
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            ),
        }),
        content=content,
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(requires_auth=OAuth2(id="arcade-asana"))
async def move_section_in_project(
    context: ToolContext,
    project_global_id: Annotated[str, "Globally unique identifier for the project in Asana."],
    enable_pretty_output: Annotated[
        bool | None,
        "If true, enables pretty output with line breaks and indentation for readability. Use primarily for debugging, as it may increase response time and size.",  # noqa: E501
    ] = None,
    insert_after_section_id: Annotated[
        str | None,
        "The ID of the section after which the given section will be inserted. Specify to reorder sections within the same project.",  # noqa: E501
    ] = None,
    insert_before_section_id: Annotated[
        str | None, "Specify the section ID to place the given section immediately before it."
    ] = None,
    section_to_reorder: Annotated[
        str | None, "Globally unique identifier for the section to move within the project."
    ] = None,
) -> Annotated[dict[str, Any], "Response from the API endpoint 'insertSectionForProject'."]:
    """Reorder sections within a project in Asana.

    Use this tool to move sections relative to each other within a single Asana project. Either 'before_section' or 'after_section' must be specified. This tool does not support moving sections between different projects."""  # noqa: E501
    request_data: Any = {
        "data": {
            "section": section_to_reorder,
            "before_section": insert_before_section_id,
            "after_section": insert_after_section_id,
        }
    }
    if isinstance(request_data, dict):
        request_data = remove_none_values(request_data)
    content = json.dumps(request_data) if request_data else None
    response = await make_request(
        url="https://app.asana.com/api/1.0/projects/{project_gid}/sections/insert".format(  # noqa: UP032
            project_gid=project_global_id
        ),
        method="POST",
        params=remove_none_values({"opt_pretty": enable_pretty_output}),
        headers=remove_none_values({
            "Content-Type": "application/json",
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            ),
        }),
        content=content,
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(requires_auth=OAuth2(id="arcade-asana"))
async def fetch_status_update(
    context: ToolContext,
    status_update_id: Annotated[
        str, "Unique identifier for the status update to retrieve the complete record."
    ],
    enable_pretty_output: Annotated[
        bool | None,
        "Set to true for a readable, pretty format with indentation. Useful for debugging.",
    ] = None,
    include_optional_fields: Annotated[
        list[str] | None,
        "A list of optional fields to include in the response. Provide the fields as a list of strings.",  # noqa: E501
    ] = None,
) -> Annotated[dict[str, Any], "Response from the API endpoint 'getStatus'."]:
    """Fetch the complete record for a specific status update.

    Use this tool to get detailed information about a specific status update by providing its unique identifier."""  # noqa: E501
    request_data: Any = None
    if isinstance(request_data, dict):
        request_data = remove_none_values(request_data)
    content = json.dumps(request_data) if request_data else None
    response = await make_request(
        url="https://app.asana.com/api/1.0/status_updates/{status_update_gid}".format(  # noqa: UP032
            status_update_gid=status_update_id
        ),
        method="GET",
        params=remove_none_values({
            "opt_pretty": enable_pretty_output,
            "opt_fields": include_optional_fields,
        }),
        headers=remove_none_values({
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            )
        }),
        content=content,
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(requires_auth=OAuth2(id="arcade-asana"))
async def delete_status_update(
    context: ToolContext,
    status_update_id: Annotated[str, "Unique ID of the status update to be deleted."],
    pretty_output: Annotated[
        bool | None,
        "If true, the response is formatted for readability with line breaks and indentation. This increases response size and time, so use for debugging.",  # noqa: E501
    ] = None,
) -> Annotated[dict[str, Any], "Response from the API endpoint 'deleteStatus'."]:
    """Delete a specific status update from Asana.

    Use this tool to remove a specific status update in Asana by providing the status update's unique ID. This is useful for managing project updates and maintaining current information. The tool confirms successful deletion."""  # noqa: E501
    request_data: Any = None
    if isinstance(request_data, dict):
        request_data = remove_none_values(request_data)
    content = json.dumps(request_data) if request_data else None
    response = await make_request(
        url="https://app.asana.com/api/1.0/status_updates/{status_update_gid}".format(  # noqa: UP032
            status_update_gid=status_update_id
        ),
        method="DELETE",
        params=remove_none_values({"opt_pretty": pretty_output}),
        headers=remove_none_values({
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            )
        }),
        content=content,
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(requires_auth=OAuth2(id="arcade-asana"))
async def get_status_updates_for_object(
    context: ToolContext,
    object_gid: Annotated[
        str,
        "Globally unique identifier (GID) for the Asana object (project, portfolio, or goal) to fetch status updates from.",  # noqa: E501
    ],
    created_since_time: Annotated[
        str | None, "Return statuses created after the specified time. Use ISO 8601 format."
    ] = None,
    enable_pretty_output: Annotated[
        bool | None,
        "Enable pretty JSON output with line breaks and indentation for readability. Mainly used for debugging, as it increases response size.",  # noqa: E501
    ] = None,
    optional_fields_to_include: Annotated[
        list[str] | None,
        "List the optional properties to include in the response. Provide as a comma-separated list of property names.",  # noqa: E501
    ] = None,
    pagination_offset_token: Annotated[
        str | None,
        "Token to fetch the next page of results. Use an offset returned from a previous paginated request.",  # noqa: E501
    ] = None,
    results_per_page: Annotated[
        int | None, "Number of results to return per page. Must be between 1 and 100."
    ] = None,
) -> Annotated[dict[str, Any], "Response from the API endpoint 'getStatusesForObject'."]:
    """Retrieve status updates for a specified object in Asana.

    Use this tool to get the compact status update records for all updates related to a specific object in Asana. It is helpful for tracking changes or updates on any Asana object."""  # noqa: E501
    request_data: Any = None
    if isinstance(request_data, dict):
        request_data = remove_none_values(request_data)
    content = json.dumps(request_data) if request_data else None
    response = await make_request(
        url="https://app.asana.com/api/1.0/status_updates",
        method="GET",
        params=remove_none_values({
            "opt_pretty": enable_pretty_output,
            "limit": results_per_page,
            "offset": pagination_offset_token,
            "parent": object_gid,
            "created_since": created_since_time,
            "opt_fields": optional_fields_to_include,
        }),
        headers=remove_none_values({
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            )
        }),
        content=content,
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(requires_auth=OAuth2(id="arcade-asana"))
async def create_status_update(
    context: ToolContext,
    mode: Annotated[
        ToolMode,
        "Operation mode: 'get_request_schema' returns the OpenAPI spec "
        "for the request body, 'execute' performs the actual operation",
    ],
    results_per_page: Annotated[
        int | None,
        "Number of status updates to return per page, between 1 and 100.  Only used when mode is 'execute'.",  # noqa: E501
    ] = None,
    pagination_offset_token: Annotated[
        str | None,
        "Token to fetch the next page of results. Received from a previous API response for pagination. If omitted, the first page is returned.  Only used when mode is 'execute'.",  # noqa: E501
    ] = None,
    include_optional_fields: Annotated[
        list[str] | None,
        "List of properties to include. Provide as an array of strings to include optional properties in the response.  Only used when mode is 'execute'.",  # noqa: E501
    ] = None,
    enable_pretty_output: Annotated[
        bool | None,
        "Set to true to receive the response in a readable, indented format. Use for debugging as it increases response time and size.  Only used when mode is 'execute'.",  # noqa: E501
    ] = None,
    request_body: Annotated[
        str | None,
        "Stringified JSON representing the request body. Required when "
        "mode is 'execute', ignored when mode is 'get_request_schema'",
    ] = None,
) -> Annotated[dict[str, Any], "Response from the API endpoint 'createStatusForObject'."]:
    """Create a new status update on an object in Asana.

    Use this tool to add a status update to an object in Asana, such as a project. It returns the full record of the newly created status update, providing insight into the update's details and context.

    Note: Understanding the request schema is necessary to properly create
    the stringified JSON input object for execution.\n\nThis operation also requires query parameters.

    Modes:
    - GET_REQUEST_SCHEMA: Returns the schema. Only call if you don't
      already have it. Do NOT call repeatedly if you already received
      the schema.
    - EXECUTE: Performs the operation with the provided request body
      JSON.\n      Note: You must also provide the required query parameters when executing.

    If you need the schema, call with mode='get_request_schema' ONCE, then execute.
    """  # noqa: E501
    if mode == ToolMode.GET_REQUEST_SCHEMA:
        return {
            "request_body_schema": REQUEST_BODY_SCHEMAS["CREATESTATUSUPDATE"],
            "instructions": (
                "Use the request_body_schema to construct a valid JSON object. "
                "Once you have populated the object following the schema "
                "structure and requirements, call this tool again with "
                "mode='execute' and the stringified JSON as the "
                "request_body parameter along with the required query parameters. "
                "Do NOT call the schema mode again - you already have "
                "the schema now."
            ),
        }

    # Mode is EXECUTE - validate parameters

    # Validate request body is provided (not None or empty string)
    # Note: Empty objects like {} are allowed - schema validation will check if valid
    if request_body is None or request_body.strip() == "":
        raise RetryableToolError(
            message="Request body is required when mode is 'execute'",
            developer_message="The request_body parameter was null or empty string",
            additional_prompt_content=(
                "The request body is required to perform this operation. "
                "Use the schema below to construct a valid JSON object, "
                "then call this tool again in execute mode with the "
                "stringified JSON as the request_body parameter.\n\n"
                "Schema:\n\n```json\n" + REQUEST_BODY_SCHEMAS["CREATESTATUSUPDATE"] + "\n```"
            ),
        )

    # Parse JSON
    try:
        request_data = json.loads(request_body)
    except json.JSONDecodeError as e:
        raise RetryableToolError(
            message=f"Invalid JSON in request body: {e!s}",
            developer_message=f"JSON parsing failed: {e!s}",
            additional_prompt_content=(
                f"The request body contains invalid JSON. Error: {e!s}\n\n"
                "Please provide a valid JSON string that matches the schema "
                "below, then call this tool again in execute mode.\n\n"
                "Schema:\n\n```json\n" + REQUEST_BODY_SCHEMAS["CREATESTATUSUPDATE"] + "\n```"
            ),
        ) from e

    response = await make_request_with_schema_validation(
        url="https://app.asana.com/api/1.0/status_updates",
        method="POST",
        request_data=request_data,
        schema=REQUEST_BODY_SCHEMAS["CREATESTATUSUPDATE"],
        params=remove_none_values({
            "opt_pretty": enable_pretty_output,
            "limit": results_per_page,
            "offset": pagination_offset_token,
            "opt_fields": include_optional_fields,
        }),
        headers=remove_none_values({
            "Content-Type": "application/json",
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            ),
        }),
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(requires_auth=OAuth2(id="arcade-asana", scopes=["stories:read"]))
async def get_asana_story(
    context: ToolContext,
    story_identifier: Annotated[
        str, "Globally unique identifier for the Asana story to be retrieved."
    ],
    enable_pretty_output: Annotated[
        bool | None,
        "Set to true to format the response with line breaks and indentation for readability. Recommended only for debugging, as it increases response size and processing time.",  # noqa: E501
    ] = None,
    include_optional_properties: Annotated[
        list[str] | None,
        "A list of optional properties to include in the story record, specified as an array of strings.",  # noqa: E501
    ] = None,
) -> Annotated[dict[str, Any], "Response from the API endpoint 'getStory'."]:
    """Fetch the full record of a specific Asana story.

    Use this tool to retrieve detailed information about a specific story in Asana. It requires the 'stories:read' scope, and optionally reads 'attachments:read' for previews and attachments."""  # noqa: E501
    request_data: Any = None
    if isinstance(request_data, dict):
        request_data = remove_none_values(request_data)
    content = json.dumps(request_data) if request_data else None
    response = await make_request(
        url="https://app.asana.com/api/1.0/stories/{story_gid}".format(story_gid=story_identifier),  # noqa: UP032
        method="GET",
        params=remove_none_values({
            "opt_pretty": enable_pretty_output,
            "opt_fields": include_optional_properties,
        }),
        headers=remove_none_values({
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            )
        }),
        content=content,
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(requires_auth=OAuth2(id="arcade-asana", scopes=["stories:write"]))
async def update_asana_story(
    context: ToolContext,
    mode: Annotated[
        ToolMode,
        "Operation mode: 'get_request_schema' returns the OpenAPI spec "
        "for the request body, 'execute' performs the actual operation",
    ],
    story_global_id: Annotated[
        str | None,
        "Globally unique identifier for the Asana story to be updated.  Required when mode is 'execute', ignored when mode is 'get_request_schema'.",  # noqa: E501
    ] = None,
    include_optional_properties: Annotated[
        list[str] | None,
        "Comma-separated list of optional properties to include in the response. Defaults to excluding some properties.  Only used when mode is 'execute'.",  # noqa: E501
    ] = None,
    enable_pretty_output: Annotated[
        bool | None,
        "Set to true for a readable, formatted response. Use for debugging as it increases response size and time.  Only used when mode is 'execute'.",  # noqa: E501
    ] = None,
    request_body: Annotated[
        str | None,
        "Stringified JSON representing the request body. Required when "
        "mode is 'execute', ignored when mode is 'get_request_schema'",
    ] = None,
) -> Annotated[dict[str, Any], "Response from the API endpoint 'updateStory'."]:
    """Update an Asana story's details.

    Use this tool to update the text of a comment story or pin/unpin comment and attachment stories in Asana. Only one of `text` or `html_text` can be updated at a time. Requires 'stories:write' permission.

    Note: Understanding the request schema is necessary to properly create
    the stringified JSON input object for execution.\n\nThis operation also requires path, query parameters.

    Modes:
    - GET_REQUEST_SCHEMA: Returns the schema. Only call if you don't
      already have it. Do NOT call repeatedly if you already received
      the schema.
    - EXECUTE: Performs the operation with the provided request body
      JSON.\n      Note: You must also provide the required path, query parameters when executing.

    If you need the schema, call with mode='get_request_schema' ONCE, then execute.
    """  # noqa: E501
    if mode == ToolMode.GET_REQUEST_SCHEMA:
        return {
            "request_body_schema": REQUEST_BODY_SCHEMAS["UPDATEASANASTORY"],
            "instructions": (
                "Use the request_body_schema to construct a valid JSON object. "
                "Once you have populated the object following the schema "
                "structure and requirements, call this tool again with "
                "mode='execute' and the stringified JSON as the "
                "request_body parameter along with the required path, query parameters. "
                "Do NOT call the schema mode again - you already have "
                "the schema now."
            ),
        }

    # Mode is EXECUTE - validate parameters
    # Validate required parameters
    missing_params = []
    if not story_global_id:
        missing_params.append(("story_global_id", "path"))

    if missing_params:
        param_names = [p[0] for p in missing_params]
        param_details = ", ".join([f"{p[0]} ({p[1]})" for p in missing_params])
        raise RetryableToolError(
            message=f"Missing required parameters: {param_names}",
            developer_message=(f"Required parameters validation failed: {param_details}"),
            additional_prompt_content=(
                f"The following required parameters are missing: "
                f"{param_details}. Please call this tool again with all "
                "required parameters."
            ),
        )

    # Validate request body is provided (not None or empty string)
    # Note: Empty objects like {} are allowed - schema validation will check if valid
    if request_body is None or request_body.strip() == "":
        raise RetryableToolError(
            message="Request body is required when mode is 'execute'",
            developer_message="The request_body parameter was null or empty string",
            additional_prompt_content=(
                "The request body is required to perform this operation. "
                "Use the schema below to construct a valid JSON object, "
                "then call this tool again in execute mode with the "
                "stringified JSON as the request_body parameter.\n\n"
                "Schema:\n\n```json\n" + REQUEST_BODY_SCHEMAS["UPDATEASANASTORY"] + "\n```"
            ),
        )

    # Parse JSON
    try:
        request_data = json.loads(request_body)
    except json.JSONDecodeError as e:
        raise RetryableToolError(
            message=f"Invalid JSON in request body: {e!s}",
            developer_message=f"JSON parsing failed: {e!s}",
            additional_prompt_content=(
                f"The request body contains invalid JSON. Error: {e!s}\n\n"
                "Please provide a valid JSON string that matches the schema "
                "below, then call this tool again in execute mode.\n\n"
                "Schema:\n\n```json\n" + REQUEST_BODY_SCHEMAS["UPDATEASANASTORY"] + "\n```"
            ),
        ) from e

    response = await make_request_with_schema_validation(
        url="https://app.asana.com/api/1.0/stories/{story_gid}".format(story_gid=story_global_id),  # noqa: UP032
        method="PUT",
        request_data=request_data,
        schema=REQUEST_BODY_SCHEMAS["UPDATEASANASTORY"],
        params=remove_none_values({
            "opt_pretty": enable_pretty_output,
            "opt_fields": include_optional_properties,
        }),
        headers=remove_none_values({
            "Content-Type": "application/json",
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            ),
        }),
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(requires_auth=OAuth2(id="arcade-asana"))
async def delete_asana_story(
    context: ToolContext,
    story_unique_id: Annotated[str, "Globally unique identifier for the story to be deleted."],
    enable_pretty_output: Annotated[
        bool | None,
        "Enable this for a readable, formatted response. Useful for debugging, but increases response time.",  # noqa: E501
    ] = None,
) -> Annotated[dict[str, Any], "Response from the API endpoint 'deleteStory'."]:
    """Delete a story you've created on Asana.

    This tool deletes a story on Asana if the story was created by the user. It should be called when a user wants to remove their own stories for tasks or projects in Asana."""  # noqa: E501
    request_data: Any = None
    if isinstance(request_data, dict):
        request_data = remove_none_values(request_data)
    content = json.dumps(request_data) if request_data else None
    response = await make_request(
        url="https://app.asana.com/api/1.0/stories/{story_gid}".format(story_gid=story_unique_id),  # noqa: UP032
        method="DELETE",
        params=remove_none_values({"opt_pretty": enable_pretty_output}),
        headers=remove_none_values({
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            )
        }),
        content=content,
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(requires_auth=OAuth2(id="arcade-asana", scopes=["stories:read"]))
async def get_task_stories(
    context: ToolContext,
    task_identifier: Annotated[str, "The unique identifier for the task to retrieve stories from."],
    enable_pretty_output: Annotated[
        bool | None,
        "Set to true for a readable, pretty format with line breaks and indentation. Useful for debugging. This will increase response time and size.",  # noqa: E501
    ] = None,
    include_optional_fields: Annotated[
        list[str] | None,
        "List of optional resource properties to include, specified as an array of strings.",
    ] = None,
    pagination_offset: Annotated[
        str | None,
        "An offset token for paginating through results. Use a token returned from a previous API call to access subsequent pages. If not provided, the first page is returned.",  # noqa: E501
    ] = None,
    results_per_page: Annotated[
        int | None, "Specify the number of stories to return per page, between 1 and 100."
    ] = None,
) -> Annotated[dict[str, Any], "Response from the API endpoint 'getStoriesForTask'."]:
    """Retrieve all stories for a specified Asana task.

    This tool fetches all compact records for stories associated with a specific task in Asana, requiring the 'stories:read' scope."""  # noqa: E501
    request_data: Any = None
    if isinstance(request_data, dict):
        request_data = remove_none_values(request_data)
    content = json.dumps(request_data) if request_data else None
    response = await make_request(
        url="https://app.asana.com/api/1.0/tasks/{task_gid}/stories".format(  # noqa: UP032
            task_gid=task_identifier
        ),
        method="GET",
        params=remove_none_values({
            "opt_pretty": enable_pretty_output,
            "limit": results_per_page,
            "offset": pagination_offset,
            "opt_fields": include_optional_fields,
        }),
        headers=remove_none_values({
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            )
        }),
        content=content,
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(requires_auth=OAuth2(id="arcade-asana", scopes=["stories:write"]))
async def add_task_comment(
    context: ToolContext,
    mode: Annotated[
        ToolMode,
        "Operation mode: 'get_request_schema' returns the OpenAPI spec "
        "for the request body, 'execute' performs the actual operation",
    ],
    target_task_id: Annotated[
        str | None,
        "The ID of the task to which the comment will be added. It is required to specify the task you want to operate on.  Required when mode is 'execute', ignored when mode is 'get_request_schema'.",  # noqa: E501
    ] = None,
    include_optional_properties: Annotated[
        list[str] | None,
        "Comma-separated list of optional properties to include in the response, as the endpoint excludes some by default.  Only used when mode is 'execute'.",  # noqa: E501
    ] = None,
    enable_pretty_output: Annotated[
        bool | None,
        "Formats the response for readability with line breaks and indentation when true. Recommended for debugging due to extra processing time.  Only used when mode is 'execute'.",  # noqa: E501
    ] = None,
    request_body: Annotated[
        str | None,
        "Stringified JSON representing the request body. Required when "
        "mode is 'execute', ignored when mode is 'get_request_schema'",
    ] = None,
) -> Annotated[dict[str, Any], "Response from the API endpoint 'createStoryForTask'."]:
    """Add a comment to a specific task in Asana.

    This tool adds a comment to a task in Asana, authored by the authenticated user, and returns the full record of the newly created comment.

    Note: Understanding the request schema is necessary to properly create
    the stringified JSON input object for execution.\n\nThis operation also requires path, query parameters.

    Modes:
    - GET_REQUEST_SCHEMA: Returns the schema. Only call if you don't
      already have it. Do NOT call repeatedly if you already received
      the schema.
    - EXECUTE: Performs the operation with the provided request body
      JSON.\n      Note: You must also provide the required path, query parameters when executing.

    If you need the schema, call with mode='get_request_schema' ONCE, then execute.
    """  # noqa: E501
    if mode == ToolMode.GET_REQUEST_SCHEMA:
        return {
            "request_body_schema": REQUEST_BODY_SCHEMAS["ADDTASKCOMMENT"],
            "instructions": (
                "Use the request_body_schema to construct a valid JSON object. "
                "Once you have populated the object following the schema "
                "structure and requirements, call this tool again with "
                "mode='execute' and the stringified JSON as the "
                "request_body parameter along with the required path, query parameters. "
                "Do NOT call the schema mode again - you already have "
                "the schema now."
            ),
        }

    # Mode is EXECUTE - validate parameters
    # Validate required parameters
    missing_params = []
    if not target_task_id:
        missing_params.append(("target_task_id", "path"))

    if missing_params:
        param_names = [p[0] for p in missing_params]
        param_details = ", ".join([f"{p[0]} ({p[1]})" for p in missing_params])
        raise RetryableToolError(
            message=f"Missing required parameters: {param_names}",
            developer_message=(f"Required parameters validation failed: {param_details}"),
            additional_prompt_content=(
                f"The following required parameters are missing: "
                f"{param_details}. Please call this tool again with all "
                "required parameters."
            ),
        )

    # Validate request body is provided (not None or empty string)
    # Note: Empty objects like {} are allowed - schema validation will check if valid
    if request_body is None or request_body.strip() == "":
        raise RetryableToolError(
            message="Request body is required when mode is 'execute'",
            developer_message="The request_body parameter was null or empty string",
            additional_prompt_content=(
                "The request body is required to perform this operation. "
                "Use the schema below to construct a valid JSON object, "
                "then call this tool again in execute mode with the "
                "stringified JSON as the request_body parameter.\n\n"
                "Schema:\n\n```json\n" + REQUEST_BODY_SCHEMAS["ADDTASKCOMMENT"] + "\n```"
            ),
        )

    # Parse JSON
    try:
        request_data = json.loads(request_body)
    except json.JSONDecodeError as e:
        raise RetryableToolError(
            message=f"Invalid JSON in request body: {e!s}",
            developer_message=f"JSON parsing failed: {e!s}",
            additional_prompt_content=(
                f"The request body contains invalid JSON. Error: {e!s}\n\n"
                "Please provide a valid JSON string that matches the schema "
                "below, then call this tool again in execute mode.\n\n"
                "Schema:\n\n```json\n" + REQUEST_BODY_SCHEMAS["ADDTASKCOMMENT"] + "\n```"
            ),
        ) from e

    response = await make_request_with_schema_validation(
        url="https://app.asana.com/api/1.0/tasks/{task_gid}/stories".format(  # noqa: UP032
            task_gid=target_task_id
        ),
        method="POST",
        request_data=request_data,
        schema=REQUEST_BODY_SCHEMAS["ADDTASKCOMMENT"],
        params=remove_none_values({
            "opt_pretty": enable_pretty_output,
            "opt_fields": include_optional_properties,
        }),
        headers=remove_none_values({
            "Content-Type": "application/json",
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            ),
        }),
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(requires_auth=OAuth2(id="arcade-asana", scopes=["tags:read"]))
async def get_filtered_tags(
    context: ToolContext,
    enable_pretty_output: Annotated[
        bool | None,
        "Set to 'true' for pretty JSON output with line breaks and indentation. Use for readability during debugging.",  # noqa: E501
    ] = None,
    include_optional_properties: Annotated[
        list[str] | None,
        "A list of properties to include in the response, specified as strings. Use to retrieve fields not returned by default.",  # noqa: E501
    ] = None,
    pagination_offset: Annotated[
        str | None,
        "Offset token for pagination. Use the offset from the previous response to get the next page. If not provided, returns the first page.",  # noqa: E501
    ] = None,
    results_per_page: Annotated[
        int | None,
        "Specifies how many tag records to return per page. Must be an integer between 1 and 100.",
    ] = None,
    workspace_for_filtering: Annotated[
        str | None, "The workspace ID used to filter tags in the request."
    ] = None,
) -> Annotated[dict[str, Any], "Response from the API endpoint 'getTags'."]:
    """Retrieve compact tag records with optional filters.

    Call this tool to get a list of compact tag records from Asana based on specific filter parameters. Requires 'tags:read' scope."""  # noqa: E501
    request_data: Any = None
    if isinstance(request_data, dict):
        request_data = remove_none_values(request_data)
    content = json.dumps(request_data) if request_data else None
    response = await make_request(
        url="https://app.asana.com/api/1.0/tags",
        method="GET",
        params=remove_none_values({
            "opt_pretty": enable_pretty_output,
            "limit": results_per_page,
            "offset": pagination_offset,
            "workspace": workspace_for_filtering,
            "opt_fields": include_optional_properties,
        }),
        headers=remove_none_values({
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            )
        }),
        content=content,
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(requires_auth=OAuth2(id="arcade-asana", scopes=["tags:write"]))
async def create_new_asana_tag(
    context: ToolContext,
    mode: Annotated[
        ToolMode,
        "Operation mode: 'get_request_schema' returns the OpenAPI spec "
        "for the request body, 'execute' performs the actual operation",
    ],
    include_optional_properties: Annotated[
        list[str] | None,
        "Specify optional properties to include in the response as a list of strings. These properties are excluded by default.  Only used when mode is 'execute'.",  # noqa: E501
    ] = None,
    pretty_output_enabled: Annotated[
        bool | None,
        "Set to true for a well-indented, readable response format. Recommend use only for debugging due to increased response size and time.  Only used when mode is 'execute'.",  # noqa: E501
    ] = None,
    request_body: Annotated[
        str | None,
        "Stringified JSON representing the request body. Required when "
        "mode is 'execute', ignored when mode is 'get_request_schema'",
    ] = None,
) -> Annotated[dict[str, Any], "Response from the API endpoint 'createTag'."]:
    """Create a new tag in an Asana workspace or organization.

    This tool should be called when there's a need to create a new tag within a specific Asana workspace or organization. It requires the 'tags:write' scope and returns the full record of the newly created tag.

    Note: Understanding the request schema is necessary to properly create
    the stringified JSON input object for execution.\n\nThis operation also requires query parameters.

    Modes:
    - GET_REQUEST_SCHEMA: Returns the schema. Only call if you don't
      already have it. Do NOT call repeatedly if you already received
      the schema.
    - EXECUTE: Performs the operation with the provided request body
      JSON.\n      Note: You must also provide the required query parameters when executing.

    If you need the schema, call with mode='get_request_schema' ONCE, then execute.
    """  # noqa: E501
    if mode == ToolMode.GET_REQUEST_SCHEMA:
        return {
            "request_body_schema": REQUEST_BODY_SCHEMAS["CREATENEWASANATAG"],
            "instructions": (
                "Use the request_body_schema to construct a valid JSON object. "
                "Once you have populated the object following the schema "
                "structure and requirements, call this tool again with "
                "mode='execute' and the stringified JSON as the "
                "request_body parameter along with the required query parameters. "
                "Do NOT call the schema mode again - you already have "
                "the schema now."
            ),
        }

    # Mode is EXECUTE - validate parameters

    # Validate request body is provided (not None or empty string)
    # Note: Empty objects like {} are allowed - schema validation will check if valid
    if request_body is None or request_body.strip() == "":
        raise RetryableToolError(
            message="Request body is required when mode is 'execute'",
            developer_message="The request_body parameter was null or empty string",
            additional_prompt_content=(
                "The request body is required to perform this operation. "
                "Use the schema below to construct a valid JSON object, "
                "then call this tool again in execute mode with the "
                "stringified JSON as the request_body parameter.\n\n"
                "Schema:\n\n```json\n" + REQUEST_BODY_SCHEMAS["CREATENEWASANATAG"] + "\n```"
            ),
        )

    # Parse JSON
    try:
        request_data = json.loads(request_body)
    except json.JSONDecodeError as e:
        raise RetryableToolError(
            message=f"Invalid JSON in request body: {e!s}",
            developer_message=f"JSON parsing failed: {e!s}",
            additional_prompt_content=(
                f"The request body contains invalid JSON. Error: {e!s}\n\n"
                "Please provide a valid JSON string that matches the schema "
                "below, then call this tool again in execute mode.\n\n"
                "Schema:\n\n```json\n" + REQUEST_BODY_SCHEMAS["CREATENEWASANATAG"] + "\n```"
            ),
        ) from e

    response = await make_request_with_schema_validation(
        url="https://app.asana.com/api/1.0/tags",
        method="POST",
        request_data=request_data,
        schema=REQUEST_BODY_SCHEMAS["CREATENEWASANATAG"],
        params=remove_none_values({
            "opt_pretty": pretty_output_enabled,
            "opt_fields": include_optional_properties,
        }),
        headers=remove_none_values({
            "Content-Type": "application/json",
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            ),
        }),
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(requires_auth=OAuth2(id="arcade-asana", scopes=["tags:read"]))
async def get_asana_tag_details(
    context: ToolContext,
    tag_global_identifier: Annotated[
        str,
        "Globally unique identifier for the tag in Asana used to fetch the complete tag details.",
    ],
    enable_pretty_output: Annotated[
        bool | None,
        "Enable to format the response in a readable, pretty format. Increases response size and time; useful for debugging.",  # noqa: E501
    ] = None,
    include_optional_fields: Annotated[
        list[str] | None,
        "Comma-separated list of additional tag properties to include in the response.",
    ] = None,
) -> Annotated[dict[str, Any], "Response from the API endpoint 'getTag'."]:
    """Retrieve complete details for a specific Asana tag.

    Fetches the full tag record for a provided tag ID in Asana. Useful for obtaining information about a specific tag to understand its properties and associated tasks."""  # noqa: E501
    request_data: Any = None
    if isinstance(request_data, dict):
        request_data = remove_none_values(request_data)
    content = json.dumps(request_data) if request_data else None
    response = await make_request(
        url="https://app.asana.com/api/1.0/tags/{tag_gid}".format(tag_gid=tag_global_identifier),  # noqa: UP032
        method="GET",
        params=remove_none_values({
            "opt_pretty": enable_pretty_output,
            "opt_fields": include_optional_fields,
        }),
        headers=remove_none_values({
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            )
        }),
        content=content,
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(requires_auth=OAuth2(id="arcade-asana", scopes=["tags:write"]))
async def update_asana_tag(
    context: ToolContext,
    mode: Annotated[
        ToolMode,
        "Operation mode: 'get_request_schema' returns the OpenAPI spec "
        "for the request body, 'execute' performs the actual operation",
    ],
    tag_global_identifier: Annotated[
        str | None,
        "Globally unique identifier for the tag to update in Asana.  Required when mode is 'execute', ignored when mode is 'get_request_schema'.",  # noqa: E501
    ] = None,
    include_optional_fields: Annotated[
        list[str] | None,
        "List of optional properties to include with the tag resource. Provide as an array of property names.  Only used when mode is 'execute'.",  # noqa: E501
    ] = None,
    enable_pretty_output: Annotated[
        bool | None,
        "Set to true to receive the response in a readable format with proper indentation and line breaks. This is advised only for debugging as it increases response size.  Only used when mode is 'execute'.",  # noqa: E501
    ] = None,
    request_body: Annotated[
        str | None,
        "Stringified JSON representing the request body. Required when "
        "mode is 'execute', ignored when mode is 'get_request_schema'",
    ] = None,
) -> Annotated[dict[str, Any], "Response from the API endpoint 'updateTag'."]:
    """Update properties of an Asana tag.

    Updates specific properties of an Asana tag using the provided tag ID. Specify only the fields you wish to change to avoid overwriting other users' changes. Returns the complete updated tag record.

    Note: Understanding the request schema is necessary to properly create
    the stringified JSON input object for execution.\n\nThis operation also requires path, query parameters.

    Modes:
    - GET_REQUEST_SCHEMA: Returns the schema. Only call if you don't
      already have it. Do NOT call repeatedly if you already received
      the schema.
    - EXECUTE: Performs the operation with the provided request body
      JSON.\n      Note: You must also provide the required path, query parameters when executing.

    If you need the schema, call with mode='get_request_schema' ONCE, then execute.
    """  # noqa: E501
    if mode == ToolMode.GET_REQUEST_SCHEMA:
        return {
            "request_body_schema": REQUEST_BODY_SCHEMAS["UPDATEASANATAG"],
            "instructions": (
                "Use the request_body_schema to construct a valid JSON object. "
                "Once you have populated the object following the schema "
                "structure and requirements, call this tool again with "
                "mode='execute' and the stringified JSON as the "
                "request_body parameter along with the required path, query parameters. "
                "Do NOT call the schema mode again - you already have "
                "the schema now."
            ),
        }

    # Mode is EXECUTE - validate parameters
    # Validate required parameters
    missing_params = []
    if not tag_global_identifier:
        missing_params.append(("tag_global_identifier", "path"))

    if missing_params:
        param_names = [p[0] for p in missing_params]
        param_details = ", ".join([f"{p[0]} ({p[1]})" for p in missing_params])
        raise RetryableToolError(
            message=f"Missing required parameters: {param_names}",
            developer_message=(f"Required parameters validation failed: {param_details}"),
            additional_prompt_content=(
                f"The following required parameters are missing: "
                f"{param_details}. Please call this tool again with all "
                "required parameters."
            ),
        )

    # Validate request body is provided (not None or empty string)
    # Note: Empty objects like {} are allowed - schema validation will check if valid
    if request_body is None or request_body.strip() == "":
        raise RetryableToolError(
            message="Request body is required when mode is 'execute'",
            developer_message="The request_body parameter was null or empty string",
            additional_prompt_content=(
                "The request body is required to perform this operation. "
                "Use the schema below to construct a valid JSON object, "
                "then call this tool again in execute mode with the "
                "stringified JSON as the request_body parameter.\n\n"
                "Schema:\n\n```json\n" + REQUEST_BODY_SCHEMAS["UPDATEASANATAG"] + "\n```"
            ),
        )

    # Parse JSON
    try:
        request_data = json.loads(request_body)
    except json.JSONDecodeError as e:
        raise RetryableToolError(
            message=f"Invalid JSON in request body: {e!s}",
            developer_message=f"JSON parsing failed: {e!s}",
            additional_prompt_content=(
                f"The request body contains invalid JSON. Error: {e!s}\n\n"
                "Please provide a valid JSON string that matches the schema "
                "below, then call this tool again in execute mode.\n\n"
                "Schema:\n\n```json\n" + REQUEST_BODY_SCHEMAS["UPDATEASANATAG"] + "\n```"
            ),
        ) from e

    response = await make_request_with_schema_validation(
        url="https://app.asana.com/api/1.0/tags/{tag_gid}".format(tag_gid=tag_global_identifier),  # noqa: UP032
        method="PUT",
        request_data=request_data,
        schema=REQUEST_BODY_SCHEMAS["UPDATEASANATAG"],
        params=remove_none_values({
            "opt_pretty": enable_pretty_output,
            "opt_fields": include_optional_fields,
        }),
        headers=remove_none_values({
            "Content-Type": "application/json",
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            ),
        }),
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(requires_auth=OAuth2(id="arcade-asana"))
async def delete_asana_tag(
    context: ToolContext,
    tag_unique_identifier: Annotated[
        str, "The globally unique identifier for the Asana tag to be deleted."
    ],
    enable_pretty_output: Annotated[
        bool | None,
        "Set to true for formatted JSON output with line breaks and indentation. Use mainly for debugging as it increases response size.",  # noqa: E501
    ] = None,
) -> Annotated[dict[str, Any], "Response from the API endpoint 'deleteTag'."]:
    """Delete a specific tag in Asana with its unique ID.

    Use this tool to delete an existing tag in Asana by providing its unique global ID (tag_gid). This operation is permanent and should be used when a tag is no longer needed."""  # noqa: E501
    request_data: Any = None
    if isinstance(request_data, dict):
        request_data = remove_none_values(request_data)
    content = json.dumps(request_data) if request_data else None
    response = await make_request(
        url="https://app.asana.com/api/1.0/tags/{tag_gid}".format(tag_gid=tag_unique_identifier),  # noqa: UP032
        method="DELETE",
        params=remove_none_values({"opt_pretty": enable_pretty_output}),
        headers=remove_none_values({
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            )
        }),
        content=content,
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(requires_auth=OAuth2(id="arcade-asana", scopes=["tags:read"]))
async def get_tags_for_task(
    context: ToolContext,
    task_identifier: Annotated[
        str, "The unique identifier of the task to retrieve tags for in Asana."
    ],
    enable_pretty_output: Annotated[
        bool | None,
        "Set to true to receive the response in a formatted, readable form. Useful for debugging as it increases response size and time.",  # noqa: E501
    ] = None,
    include_optional_fields: Annotated[
        list[str] | None,
        "List of optional properties to include in the response as comma-separated values.",
    ] = None,
    pagination_offset_token: Annotated[
        str | None,
        "An offset token for paginating results. Use the token from a previous response to access subsequent pages.",  # noqa: E501
    ] = None,
    results_per_page: Annotated[
        int | None, "Number of objects to return per page. Must be between 1 and 100."
    ] = None,
) -> Annotated[dict[str, Any], "Response from the API endpoint 'getTagsForTask'."]:
    """Retrieve all tags for a given task.

    Use this tool to get a list of tags linked to a specific task in Asana. It requires the 'tags:read' scope to access the data."""  # noqa: E501
    request_data: Any = None
    if isinstance(request_data, dict):
        request_data = remove_none_values(request_data)
    content = json.dumps(request_data) if request_data else None
    response = await make_request(
        url="https://app.asana.com/api/1.0/tasks/{task_gid}/tags".format(task_gid=task_identifier),  # noqa: UP032
        method="GET",
        params=remove_none_values({
            "opt_pretty": enable_pretty_output,
            "limit": results_per_page,
            "offset": pagination_offset_token,
            "opt_fields": include_optional_fields,
        }),
        headers=remove_none_values({
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            )
        }),
        content=content,
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(requires_auth=OAuth2(id="arcade-asana", scopes=["tags:read"]))
async def retrieve_workspace_tags(
    context: ToolContext,
    workspace_identifier: Annotated[
        str, "Globally unique identifier for the Asana workspace or organization."
    ],
    enable_pretty_output: Annotated[
        bool | None,
        "Enable pretty JSON output with proper line breaking and indentation intended for debugging. This may increase response time and size.",  # noqa: E501
    ] = None,
    include_optional_properties: Annotated[
        list[str] | None,
        "List of optional tag properties to include, specified as strings. These properties are excluded by default.",  # noqa: E501
    ] = None,
    offset_token: Annotated[
        str | None,
        "The offset token to retrieve the next page of results. Use a token from a previous response for pagination.",  # noqa: E501
    ] = None,
    results_per_page: Annotated[
        int | None, "Specify the number of tag records to return per page, between 1 and 100."
    ] = None,
) -> Annotated[dict[str, Any], "Response from the API endpoint 'getTagsForWorkspace'."]:
    """Retrieve tags for a specific workspace in Asana.

    Use this tool to get compact tag records for a specified workspace in Asana. The tags can be filtered using various parameters."""  # noqa: E501
    request_data: Any = None
    if isinstance(request_data, dict):
        request_data = remove_none_values(request_data)
    content = json.dumps(request_data) if request_data else None
    response = await make_request(
        url="https://app.asana.com/api/1.0/workspaces/{workspace_gid}/tags".format(  # noqa: UP032
            workspace_gid=workspace_identifier
        ),
        method="GET",
        params=remove_none_values({
            "opt_pretty": enable_pretty_output,
            "limit": results_per_page,
            "offset": offset_token,
            "opt_fields": include_optional_properties,
        }),
        headers=remove_none_values({
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            )
        }),
        content=content,
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(requires_auth=OAuth2(id="arcade-asana", scopes=["tags:write"]))
async def create_workspace_tag(
    context: ToolContext,
    mode: Annotated[
        ToolMode,
        "Operation mode: 'get_request_schema' returns the OpenAPI spec "
        "for the request body, 'execute' performs the actual operation",
    ],
    workspace_id: Annotated[
        str | None,
        "Globally unique identifier for the workspace or organization in Asana.  Required when mode is 'execute', ignored when mode is 'get_request_schema'.",  # noqa: E501
    ] = None,
    included_optional_properties: Annotated[
        list[str] | None,
        "List of optional properties to include in the response, specified as an array of strings.  Only used when mode is 'execute'.",  # noqa: E501
    ] = None,
    enable_pretty_output: Annotated[
        bool | None,
        "Enable pretty JSON formatting for improved readability. Use mainly during debugging as it may increase response size.  Only used when mode is 'execute'.",  # noqa: E501
    ] = None,
    request_body: Annotated[
        str | None,
        "Stringified JSON representing the request body. Required when "
        "mode is 'execute', ignored when mode is 'get_request_schema'",
    ] = None,
) -> Annotated[dict[str, Any], "Response from the API endpoint 'createTagForWorkspace'."]:
    """Create a new tag in a specific Asana workspace.

    This tool creates a new tag in a designated workspace or organization in Asana. The tag is permanently associated with the specified workspace. It returns the full details of the newly created tag. This is useful for organizing tasks with a new categorization.

    Note: Understanding the request schema is necessary to properly create
    the stringified JSON input object for execution.\n\nThis operation also requires path, query parameters.

    Modes:
    - GET_REQUEST_SCHEMA: Returns the schema. Only call if you don't
      already have it. Do NOT call repeatedly if you already received
      the schema.
    - EXECUTE: Performs the operation with the provided request body
      JSON.\n      Note: You must also provide the required path, query parameters when executing.

    If you need the schema, call with mode='get_request_schema' ONCE, then execute.
    """  # noqa: E501
    if mode == ToolMode.GET_REQUEST_SCHEMA:
        return {
            "request_body_schema": REQUEST_BODY_SCHEMAS["CREATEWORKSPACETAG"],
            "instructions": (
                "Use the request_body_schema to construct a valid JSON object. "
                "Once you have populated the object following the schema "
                "structure and requirements, call this tool again with "
                "mode='execute' and the stringified JSON as the "
                "request_body parameter along with the required path, query parameters. "
                "Do NOT call the schema mode again - you already have "
                "the schema now."
            ),
        }

    # Mode is EXECUTE - validate parameters
    # Validate required parameters
    missing_params = []
    if not workspace_id:
        missing_params.append(("workspace_id", "path"))

    if missing_params:
        param_names = [p[0] for p in missing_params]
        param_details = ", ".join([f"{p[0]} ({p[1]})" for p in missing_params])
        raise RetryableToolError(
            message=f"Missing required parameters: {param_names}",
            developer_message=(f"Required parameters validation failed: {param_details}"),
            additional_prompt_content=(
                f"The following required parameters are missing: "
                f"{param_details}. Please call this tool again with all "
                "required parameters."
            ),
        )

    # Validate request body is provided (not None or empty string)
    # Note: Empty objects like {} are allowed - schema validation will check if valid
    if request_body is None or request_body.strip() == "":
        raise RetryableToolError(
            message="Request body is required when mode is 'execute'",
            developer_message="The request_body parameter was null or empty string",
            additional_prompt_content=(
                "The request body is required to perform this operation. "
                "Use the schema below to construct a valid JSON object, "
                "then call this tool again in execute mode with the "
                "stringified JSON as the request_body parameter.\n\n"
                "Schema:\n\n```json\n" + REQUEST_BODY_SCHEMAS["CREATEWORKSPACETAG"] + "\n```"
            ),
        )

    # Parse JSON
    try:
        request_data = json.loads(request_body)
    except json.JSONDecodeError as e:
        raise RetryableToolError(
            message=f"Invalid JSON in request body: {e!s}",
            developer_message=f"JSON parsing failed: {e!s}",
            additional_prompt_content=(
                f"The request body contains invalid JSON. Error: {e!s}\n\n"
                "Please provide a valid JSON string that matches the schema "
                "below, then call this tool again in execute mode.\n\n"
                "Schema:\n\n```json\n" + REQUEST_BODY_SCHEMAS["CREATEWORKSPACETAG"] + "\n```"
            ),
        ) from e

    response = await make_request_with_schema_validation(
        url="https://app.asana.com/api/1.0/workspaces/{workspace_gid}/tags".format(  # noqa: UP032
            workspace_gid=workspace_id
        ),
        method="POST",
        request_data=request_data,
        schema=REQUEST_BODY_SCHEMAS["CREATEWORKSPACETAG"],
        params=remove_none_values({
            "opt_pretty": enable_pretty_output,
            "opt_fields": included_optional_properties,
        }),
        headers=remove_none_values({
            "Content-Type": "application/json",
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            ),
        }),
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(requires_auth=OAuth2(id="arcade-asana", scopes=["task_templates:read"]))
async def fetch_project_task_templates(
    context: ToolContext,
    enable_pretty_output: Annotated[
        bool | None,
        "Enable this to receive the response in a readable format with proper indentation. Useful for debugging, but increases response time and size.",  # noqa: E501
    ] = None,
    optional_fields_to_include: Annotated[
        list[str] | None,
        "A list of property names to include in the response. Specify the properties you wish to see for the task templates.",  # noqa: E501
    ] = None,
    pagination_offset_token: Annotated[
        str | None,
        "Offset token for pagination, returned by the API. Use to request the next page of results.",  # noqa: E501
    ] = None,
    project_id: Annotated[
        str | None, "The unique identifier for the project to filter task templates."
    ] = None,
    results_per_page: Annotated[
        int | None, "Number of task templates to return per page, between 1 and 100."
    ] = None,
) -> Annotated[dict[str, Any], "Response from the API endpoint 'getTaskTemplates'."]:
    """Retrieve compact task template records for a specific project.

    Call this tool to obtain a list of compact task template records from Asana for a given project. This can be useful when you need to view or manage task templates associated with a particular project scope."""  # noqa: E501
    request_data: Any = None
    if isinstance(request_data, dict):
        request_data = remove_none_values(request_data)
    content = json.dumps(request_data) if request_data else None
    response = await make_request(
        url="https://app.asana.com/api/1.0/task_templates",
        method="GET",
        params=remove_none_values({
            "opt_pretty": enable_pretty_output,
            "limit": results_per_page,
            "offset": pagination_offset_token,
            "project": project_id,
            "opt_fields": optional_fields_to_include,
        }),
        headers=remove_none_values({
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            )
        }),
        content=content,
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(requires_auth=OAuth2(id="arcade-asana", scopes=["task_templates:read"]))
async def fetch_task_template(
    context: ToolContext,
    task_template_unique_id: Annotated[
        str, "Globally unique identifier for the task template to retrieve its complete record."
    ],
    enable_pretty_output: Annotated[
        bool | None,
        "Enable pretty formatting for the output, making it more readable with line breaking and indentation. Use mainly for debugging.",  # noqa: E501
    ] = None,
    include_optional_fields: Annotated[
        list[str] | None,
        "Specify properties to include in the response. Provide as a list of strings.",
    ] = None,
) -> Annotated[dict[str, Any], "Response from the API endpoint 'getTaskTemplate'."]:
    """Retrieve the complete record of a specific task template in Asana.

    Use this tool to get detailed information about a task template from Asana by providing the task template's GID. Make sure your token has the 'task_templates:read' scope."""  # noqa: E501
    request_data: Any = None
    if isinstance(request_data, dict):
        request_data = remove_none_values(request_data)
    content = json.dumps(request_data) if request_data else None
    response = await make_request(
        url="https://app.asana.com/api/1.0/task_templates/{task_template_gid}".format(  # noqa: UP032
            task_template_gid=task_template_unique_id
        ),
        method="GET",
        params=remove_none_values({
            "opt_pretty": enable_pretty_output,
            "opt_fields": include_optional_fields,
        }),
        headers=remove_none_values({
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            )
        }),
        content=content,
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(requires_auth=OAuth2(id="arcade-asana"))
async def delete_task_template(
    context: ToolContext,
    task_template_id: Annotated[
        str, "Globally unique identifier for the task template to be deleted."
    ],
    enable_pretty_output: Annotated[
        bool | None,
        "Set to True for a pretty, formatted JSON response. This is useful for debugging but may increase response time and size.",  # noqa: E501
    ] = None,
) -> Annotated[dict[str, Any], "Response from the API endpoint 'deleteTaskTemplate'."]:
    """Delete a specific task template by its ID.

    Use this tool to delete an existing task template in Asana by providing its unique GID. It's useful for removing templates that are no longer needed."""  # noqa: E501
    request_data: Any = None
    if isinstance(request_data, dict):
        request_data = remove_none_values(request_data)
    content = json.dumps(request_data) if request_data else None
    response = await make_request(
        url="https://app.asana.com/api/1.0/task_templates/{task_template_gid}".format(  # noqa: UP032
            task_template_gid=task_template_id
        ),
        method="DELETE",
        params=remove_none_values({"opt_pretty": enable_pretty_output}),
        headers=remove_none_values({
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            )
        }),
        content=content,
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(requires_auth=OAuth2(id="arcade-asana"))
async def create_asana_task(
    context: ToolContext,
    task_template_id: Annotated[
        str, "Globally unique identifier for the task template used to create the task."
    ],
    enable_pretty_output: Annotated[
        bool | None,
        "Enable 'pretty' formatting of the response for better readability. Useful for debugging but may increase response time and size.",  # noqa: E501
    ] = None,
    include_optional_properties: Annotated[
        list[str] | None,
        "List of optional properties to include in the task resource, comma-separated.",
    ] = None,
    task_name: Annotated[
        str | None,
        "The name of the new task to be created. If not provided, the task template name will be used by default.",  # noqa: E501
    ] = None,
) -> Annotated[dict[str, Any], "Response from the API endpoint 'instantiateTask'."]:
    """Create and initiate an Asana task asynchronously.

    This tool creates a job to instantiate a task in Asana using a specified task template. It should be called when you want to initiate a task based on a template, resulting in the creation of an asynchronous job that returns the job details."""  # noqa: E501
    request_data: Any = {"data": {"name": task_name}}
    if isinstance(request_data, dict):
        request_data = remove_none_values(request_data)
    content = json.dumps(request_data) if request_data else None
    response = await make_request(
        url="https://app.asana.com/api/1.0/task_templates/{task_template_gid}/instantiateTask".format(  # noqa: UP032
            task_template_gid=task_template_id
        ),
        method="POST",
        params=remove_none_values({
            "opt_pretty": enable_pretty_output,
            "opt_fields": include_optional_properties,
        }),
        headers=remove_none_values({
            "Content-Type": "application/json",
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            ),
        }),
        content=content,
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(requires_auth=OAuth2(id="arcade-asana", scopes=["tasks:read"]))
async def get_filtered_tasks(
    context: ToolContext,
    assignee_id: Annotated[
        str | None,
        "The ID of the assignee to filter tasks on. If searching for unassigned tasks, use 'null'. Must be used with 'workspace'.",  # noqa: E501
    ] = None,
    completed_since_date_time: Annotated[
        str | None,
        "Tasks must be incomplete or completed since this date/time. Provide in ISO 8601 format (e.g., '2023-10-01T12:00:00Z').",  # noqa: E501
    ] = None,
    enable_pretty_output: Annotated[
        bool | None,
        "If true, provides the response in a pretty-printed, readable format. Use primarily for debugging as it increases response time and size.",  # noqa: E501
    ] = None,
    filter_by_project: Annotated[
        str | None, "Specify the project to filter tasks. Use a string identifier for the project."
    ] = None,
    filter_by_workspace: Annotated[
        str | None, "The workspace to filter tasks by. Must be used with 'filter_by_assignee'."
    ] = None,
    modified_since_time: Annotated[
        str | None,
        "Return tasks modified since this time. Include changes in properties or associations. Format as string (e.g., 'YYYY-MM-DDTHH:MM:SSZ').",  # noqa: E501
    ] = None,
    optional_fields_to_include: Annotated[
        list[str] | None,
        "Specify properties to include in the response by providing an array of property names. These properties are excluded by default.",  # noqa: E501
    ] = None,
    pagination_offset: Annotated[
        str | None,
        "An offset token for pagination to retrieve the next page of results. Use a previously returned token to continue pagination.",  # noqa: E501
    ] = None,
    results_per_page: Annotated[
        int | None,
        "Defines the number of task records to return per page. The value must be between 1 and 100.",  # noqa: E501
    ] = None,
    section_filter: Annotated[
        str | None,
        "The section to filter tasks within a project in Asana. Specify a section name to narrow down the tasks.",  # noqa: E501
    ] = None,
) -> Annotated[dict[str, Any], "Response from the API endpoint 'getTasks'."]:
    """Retrieve filtered task records from Asana.

    Use this tool to get compact task records based on specific filters like project, tag, assignee, or workspace. Ensure to specify a `project` or `tag` if `assignee` and `workspace` are not provided."""  # noqa: E501
    request_data: Any = None
    if isinstance(request_data, dict):
        request_data = remove_none_values(request_data)
    content = json.dumps(request_data) if request_data else None
    response = await make_request(
        url="https://app.asana.com/api/1.0/tasks",
        method="GET",
        params=remove_none_values({
            "opt_pretty": enable_pretty_output,
            "limit": results_per_page,
            "offset": pagination_offset,
            "assignee": assignee_id,
            "project": filter_by_project,
            "section": section_filter,
            "workspace": filter_by_workspace,
            "completed_since": completed_since_date_time,
            "modified_since": modified_since_time,
            "opt_fields": optional_fields_to_include,
        }),
        headers=remove_none_values({
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            )
        }),
        content=content,
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(requires_auth=OAuth2(id="arcade-asana", scopes=["tasks:write"]))
async def add_asana_task(
    context: ToolContext,
    mode: Annotated[
        ToolMode,
        "Operation mode: 'get_request_schema' returns the OpenAPI spec "
        "for the request body, 'execute' performs the actual operation",
    ],
    include_optional_fields: Annotated[
        list[str] | None,
        "Comma-separated list of optional task properties to include in the response. Excludes some properties by default.  Only used when mode is 'execute'.",  # noqa: E501
    ] = None,
    enable_pretty_output: Annotated[
        bool | None,
        "Enable to receive the response in a readable format with line breaks and indentation. Use for debugging as it increases response time and size.  Only used when mode is 'execute'.",  # noqa: E501
    ] = None,
    request_body: Annotated[
        str | None,
        "Stringified JSON representing the request body. Required when "
        "mode is 'execute', ignored when mode is 'get_request_schema'",
    ] = None,
) -> Annotated[dict[str, Any], "Response from the API endpoint 'createTask'."]:
    """Create a new task in Asana.

    Use this tool to create a new task in a specified Asana workspace. You can set various fields on the task upon creation. It is required to specify a workspace, which cannot be changed later, unless specified through projects or a parent task.

    Note: Understanding the request schema is necessary to properly create
    the stringified JSON input object for execution.\n\nThis operation also requires query parameters.

    Modes:
    - GET_REQUEST_SCHEMA: Returns the schema. Only call if you don't
      already have it. Do NOT call repeatedly if you already received
      the schema.
    - EXECUTE: Performs the operation with the provided request body
      JSON.\n      Note: You must also provide the required query parameters when executing.

    If you need the schema, call with mode='get_request_schema' ONCE, then execute.
    """  # noqa: E501
    if mode == ToolMode.GET_REQUEST_SCHEMA:
        return {
            "request_body_schema": REQUEST_BODY_SCHEMAS["ADDASANATASK"],
            "instructions": (
                "Use the request_body_schema to construct a valid JSON object. "
                "Once you have populated the object following the schema "
                "structure and requirements, call this tool again with "
                "mode='execute' and the stringified JSON as the "
                "request_body parameter along with the required query parameters. "
                "Do NOT call the schema mode again - you already have "
                "the schema now."
            ),
        }

    # Mode is EXECUTE - validate parameters

    # Validate request body is provided (not None or empty string)
    # Note: Empty objects like {} are allowed - schema validation will check if valid
    if request_body is None or request_body.strip() == "":
        raise RetryableToolError(
            message="Request body is required when mode is 'execute'",
            developer_message="The request_body parameter was null or empty string",
            additional_prompt_content=(
                "The request body is required to perform this operation. "
                "Use the schema below to construct a valid JSON object, "
                "then call this tool again in execute mode with the "
                "stringified JSON as the request_body parameter.\n\n"
                "Schema:\n\n```json\n" + REQUEST_BODY_SCHEMAS["ADDASANATASK"] + "\n```"
            ),
        )

    # Parse JSON
    try:
        request_data = json.loads(request_body)
    except json.JSONDecodeError as e:
        raise RetryableToolError(
            message=f"Invalid JSON in request body: {e!s}",
            developer_message=f"JSON parsing failed: {e!s}",
            additional_prompt_content=(
                f"The request body contains invalid JSON. Error: {e!s}\n\n"
                "Please provide a valid JSON string that matches the schema "
                "below, then call this tool again in execute mode.\n\n"
                "Schema:\n\n```json\n" + REQUEST_BODY_SCHEMAS["ADDASANATASK"] + "\n```"
            ),
        ) from e

    response = await make_request_with_schema_validation(
        url="https://app.asana.com/api/1.0/tasks",
        method="POST",
        request_data=request_data,
        schema=REQUEST_BODY_SCHEMAS["ADDASANATASK"],
        params=remove_none_values({
            "opt_pretty": enable_pretty_output,
            "opt_fields": include_optional_fields,
        }),
        headers=remove_none_values({
            "Content-Type": "application/json",
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            ),
        }),
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(requires_auth=OAuth2(id="arcade-asana", scopes=["tasks:read"]))
async def get_task_details(
    context: ToolContext,
    task_identifier: Annotated[
        str, "Specify the unique identifier of the Asana task to retrieve detailed information."
    ],
    enable_pretty_output: Annotated[
        bool | None,
        "Set to true for a prettily formatted response, which may increase response time and size. Best used for debugging.",  # noqa: E501
    ] = None,
    include_optional_properties: Annotated[
        list[str] | None,
        "Specify optional task properties to include in the response. Use a list of strings representing property names.",  # noqa: E501
    ] = None,
) -> Annotated[dict[str, Any], "Response from the API endpoint 'getTask'."]:
    """Fetch detailed information for a specific Asana task.

    Retrieves the complete task record for a specified task in Asana. This includes information such as memberships and actual time spent, depending on permissions. Useful for getting comprehensive task data for project management and tracking purposes."""  # noqa: E501
    request_data: Any = None
    if isinstance(request_data, dict):
        request_data = remove_none_values(request_data)
    content = json.dumps(request_data) if request_data else None
    response = await make_request(
        url="https://app.asana.com/api/1.0/tasks/{task_gid}".format(task_gid=task_identifier),  # noqa: UP032
        method="GET",
        params=remove_none_values({
            "opt_pretty": enable_pretty_output,
            "opt_fields": include_optional_properties,
        }),
        headers=remove_none_values({
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            )
        }),
        content=content,
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(requires_auth=OAuth2(id="arcade-asana", scopes=["tasks:write"]))
async def update_task(
    context: ToolContext,
    mode: Annotated[
        ToolMode,
        "Operation mode: 'get_request_schema' returns the OpenAPI spec "
        "for the request body, 'execute' performs the actual operation",
    ],
    task_id: Annotated[
        str | None,
        "The unique identifier of the task to update in Asana.  Required when mode is 'execute', ignored when mode is 'get_request_schema'.",  # noqa: E501
    ] = None,
    include_optional_properties: Annotated[
        list[str] | None,
        "Comma-separated list of task properties to include in the response, such as 'created_at', 'due_date', etc.  Only used when mode is 'execute'.",  # noqa: E501
    ] = None,
    pretty_output_enabled: Annotated[
        bool | None,
        "Set to true for a formatted response with line breaks and indentation. Useful for debugging.  Only used when mode is 'execute'.",  # noqa: E501
    ] = None,
    request_body: Annotated[
        str | None,
        "Stringified JSON representing the request body. Required when "
        "mode is 'execute', ignored when mode is 'get_request_schema'",
    ] = None,
) -> Annotated[dict[str, Any], "Response from the API endpoint 'updateTask'."]:
    """Update specific fields of an existing Asana task.

    Allows updating of specific fields in a task on Asana using the task's unique ID. Only the specified fields are altered, preventing unintended overwrite of other data. Requires 'tasks:write' scope.

    Note: Understanding the request schema is necessary to properly create
    the stringified JSON input object for execution.\n\nThis operation also requires path, query parameters.

    Modes:
    - GET_REQUEST_SCHEMA: Returns the schema. Only call if you don't
      already have it. Do NOT call repeatedly if you already received
      the schema.
    - EXECUTE: Performs the operation with the provided request body
      JSON.\n      Note: You must also provide the required path, query parameters when executing.

    If you need the schema, call with mode='get_request_schema' ONCE, then execute.
    """  # noqa: E501
    if mode == ToolMode.GET_REQUEST_SCHEMA:
        return {
            "request_body_schema": REQUEST_BODY_SCHEMAS["UPDATETASK"],
            "instructions": (
                "Use the request_body_schema to construct a valid JSON object. "
                "Once you have populated the object following the schema "
                "structure and requirements, call this tool again with "
                "mode='execute' and the stringified JSON as the "
                "request_body parameter along with the required path, query parameters. "
                "Do NOT call the schema mode again - you already have "
                "the schema now."
            ),
        }

    # Mode is EXECUTE - validate parameters
    # Validate required parameters
    missing_params = []
    if not task_id:
        missing_params.append(("task_id", "path"))

    if missing_params:
        param_names = [p[0] for p in missing_params]
        param_details = ", ".join([f"{p[0]} ({p[1]})" for p in missing_params])
        raise RetryableToolError(
            message=f"Missing required parameters: {param_names}",
            developer_message=(f"Required parameters validation failed: {param_details}"),
            additional_prompt_content=(
                f"The following required parameters are missing: "
                f"{param_details}. Please call this tool again with all "
                "required parameters."
            ),
        )

    # Validate request body is provided (not None or empty string)
    # Note: Empty objects like {} are allowed - schema validation will check if valid
    if request_body is None or request_body.strip() == "":
        raise RetryableToolError(
            message="Request body is required when mode is 'execute'",
            developer_message="The request_body parameter was null or empty string",
            additional_prompt_content=(
                "The request body is required to perform this operation. "
                "Use the schema below to construct a valid JSON object, "
                "then call this tool again in execute mode with the "
                "stringified JSON as the request_body parameter.\n\n"
                "Schema:\n\n```json\n" + REQUEST_BODY_SCHEMAS["UPDATETASK"] + "\n```"
            ),
        )

    # Parse JSON
    try:
        request_data = json.loads(request_body)
    except json.JSONDecodeError as e:
        raise RetryableToolError(
            message=f"Invalid JSON in request body: {e!s}",
            developer_message=f"JSON parsing failed: {e!s}",
            additional_prompt_content=(
                f"The request body contains invalid JSON. Error: {e!s}\n\n"
                "Please provide a valid JSON string that matches the schema "
                "below, then call this tool again in execute mode.\n\n"
                "Schema:\n\n```json\n" + REQUEST_BODY_SCHEMAS["UPDATETASK"] + "\n```"
            ),
        ) from e

    response = await make_request_with_schema_validation(
        url="https://app.asana.com/api/1.0/tasks/{task_gid}".format(task_gid=task_id),  # noqa: UP032
        method="PUT",
        request_data=request_data,
        schema=REQUEST_BODY_SCHEMAS["UPDATETASK"],
        params=remove_none_values({
            "opt_pretty": pretty_output_enabled,
            "opt_fields": include_optional_properties,
        }),
        headers=remove_none_values({
            "Content-Type": "application/json",
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            ),
        }),
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(requires_auth=OAuth2(id="arcade-asana", scopes=["tasks:delete"]))
async def delete_asana_task(
    context: ToolContext,
    task_identifier: Annotated[str, "The unique identifier of the task to delete in Asana."],
    pretty_output_enabled: Annotated[
        bool | None,
        "Set to true for 'pretty' formatted response, useful for debugging. May increase response size and time.",  # noqa: E501
    ] = None,
) -> Annotated[dict[str, Any], "Response from the API endpoint 'deleteTask'."]:
    """Delete an existing task in Asana, moving it to trash.

    Use this tool to delete a specific task in Asana. The task will be moved to the user's trash, where it can be recovered within 30 days. This tool is useful for managing tasks that are no longer needed or were created by mistake."""  # noqa: E501
    request_data: Any = None
    if isinstance(request_data, dict):
        request_data = remove_none_values(request_data)
    content = json.dumps(request_data) if request_data else None
    response = await make_request(
        url="https://app.asana.com/api/1.0/tasks/{task_gid}".format(task_gid=task_identifier),  # noqa: UP032
        method="DELETE",
        params=remove_none_values({"opt_pretty": pretty_output_enabled}),
        headers=remove_none_values({
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            )
        }),
        content=content,
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(requires_auth=OAuth2(id="arcade-asana", scopes=["tasks:write"]))
async def duplicate_asana_task(
    context: ToolContext,
    task_gid: Annotated[str, "The unique identifier of the task to duplicate in Asana."],
    enable_pretty_output: Annotated[
        bool | None,
        "Enable pretty printing of the JSON response for improved readability. Use this primarily for debugging as it increases response size and time.",  # noqa: E501
    ] = None,
    fields_to_duplicate: Annotated[
        str | None,
        "Comma-separated list of task fields to duplicate, such as assignee, attachments, dates, etc.",  # noqa: E501
    ] = None,
    new_task_name: Annotated[str | None, "The name for the newly duplicated task in Asana."] = None,
    optional_fields_to_include: Annotated[
        list[str] | None, "Comma-separated list of optional properties to include in the response."
    ] = None,
) -> Annotated[dict[str, Any], "Response from the API endpoint 'duplicateTask'."]:
    """Create a job to duplicate a task in Asana.

    This tool creates and returns a job that will asynchronously handle the duplication of a specified task in Asana. It should be called when there's a need to copy an existing task."""  # noqa: E501
    request_data: Any = {"data": {"name": new_task_name, "include": fields_to_duplicate}}
    if isinstance(request_data, dict):
        request_data = remove_none_values(request_data)
    content = json.dumps(request_data) if request_data else None
    response = await make_request(
        url="https://app.asana.com/api/1.0/tasks/{task_gid}/duplicate".format(task_gid=task_gid),  # noqa: UP032
        method="POST",
        params=remove_none_values({
            "opt_pretty": enable_pretty_output,
            "opt_fields": optional_fields_to_include,
        }),
        headers=remove_none_values({
            "Content-Type": "application/json",
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            ),
        }),
        content=content,
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(requires_auth=OAuth2(id="arcade-asana", scopes=["tasks:read"]))
async def get_project_tasks(
    context: ToolContext,
    project_global_id: Annotated[
        str,
        "Globally unique identifier for the Asana project. Required to retrieve tasks from a specific project.",  # noqa: E501
    ],
    completed_since: Annotated[
        str | None, "Return tasks incomplete or completed after this date-time or 'now'."
    ] = None,
    enable_pretty_output: Annotated[
        bool | None,
        "Enable pretty formatting for the response. This makes the output more readable but increases response size and time. Useful for debugging.",  # noqa: E501
    ] = None,
    include_optional_fields: Annotated[
        list[str] | None, "Comma-separated list of optional properties to include in the response."
    ] = None,
    pagination_offset_token: Annotated[
        str | None,
        "Token for paginating results. Use it to retrieve the next set of tasks from a previously returned offset.",  # noqa: E501
    ] = None,
    results_per_page: Annotated[
        int | None,
        "Number of task records to return per page in the result. Must be between 1 and 100.",
    ] = None,
) -> Annotated[dict[str, Any], "Response from the API endpoint 'getTasksForProject'."]:
    """Fetch tasks from a specific Asana project.

    Retrieves compact task records for all tasks in a given Asana project, ordered by their priority. Useful for accessing and managing task lists."""  # noqa: E501
    request_data: Any = None
    if isinstance(request_data, dict):
        request_data = remove_none_values(request_data)
    content = json.dumps(request_data) if request_data else None
    response = await make_request(
        url="https://app.asana.com/api/1.0/projects/{project_gid}/tasks".format(  # noqa: UP032
            project_gid=project_global_id
        ),
        method="GET",
        params=remove_none_values({
            "completed_since": completed_since,
            "opt_pretty": enable_pretty_output,
            "limit": results_per_page,
            "offset": pagination_offset_token,
            "opt_fields": include_optional_fields,
        }),
        headers=remove_none_values({
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            )
        }),
        content=content,
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(requires_auth=OAuth2(id="arcade-asana", scopes=["tasks:read"]))
async def get_tasks_for_section(
    context: ToolContext,
    section_unique_identifier: Annotated[
        str,
        "The globally unique identifier for the section in Asana to fetch tasks from. Required to specify the exact section targeted.",  # noqa: E501
    ],
    completed_since_filter: Annotated[
        str | None,
        "Return tasks incomplete or completed since a specific time. Use a date-time string or 'now'.",  # noqa: E501
    ] = None,
    enable_pretty_output: Annotated[
        bool | None,
        "Enable pretty formatting of the response for readability. Increases response time and size. Use mainly for debugging.",  # noqa: E501
    ] = None,
    optional_fields_to_include: Annotated[
        list[str] | None, "Comma-separated list of optional properties to include in the response."
    ] = None,
    pagination_offset: Annotated[
        str | None,
        "Offset token for pagination. Use it to fetch the next page of results. If not provided, the first page will be returned.",  # noqa: E501
    ] = None,
    results_per_page: Annotated[
        int | None, "Number of tasks to return per page, between 1 and 100."
    ] = None,
) -> Annotated[dict[str, Any], "Response from the API endpoint 'getTasksForSection'."]:
    """Fetch tasks for a specific section in Asana.

    Use this tool to retrieve all tasks within a given section in Asana's board view. Requires 'tasks:read' scope."""  # noqa: E501
    request_data: Any = None
    if isinstance(request_data, dict):
        request_data = remove_none_values(request_data)
    content = json.dumps(request_data) if request_data else None
    response = await make_request(
        url="https://app.asana.com/api/1.0/sections/{section_gid}/tasks".format(  # noqa: UP032
            section_gid=section_unique_identifier
        ),
        method="GET",
        params=remove_none_values({
            "opt_pretty": enable_pretty_output,
            "limit": results_per_page,
            "offset": pagination_offset,
            "completed_since": completed_since_filter,
            "opt_fields": optional_fields_to_include,
        }),
        headers=remove_none_values({
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            )
        }),
        content=content,
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(requires_auth=OAuth2(id="arcade-asana", scopes=["tasks:read"]))
async def get_tasks_for_tag(
    context: ToolContext,
    tag_global_id: Annotated[
        str, "Globally unique identifier for the tag to retrieve associated tasks."
    ],
    enable_pretty_output: Annotated[
        bool | None,
        "Formats the response for readability with line breaks and indentation. Use true only during debugging due to increased response size.",  # noqa: E501
    ] = None,
    optional_fields_to_include: Annotated[
        list[str] | None,
        "List of optional property names to include in the response for each task (comma-separated).",  # noqa: E501
    ] = None,
    pagination_offset: Annotated[
        str | None,
        "A token for pagination to fetch the next set of results. Use the token returned from a previous call for fetching subsequent pages. If not provided, the first page of results is returned.",  # noqa: E501
    ] = None,
    results_per_page: Annotated[
        int | None, "The number of task objects to return per page, ranging from 1 to 100."
    ] = None,
) -> Annotated[dict[str, Any], "Response from the API endpoint 'getTasksForTag'."]:
    """Retrieve tasks associated with a specific tag in Asana.

    Use this tool to get a list of tasks that are associated with a specific tag in Asana. This can help organize tasks that share common attributes or objectives. Useful for managing tasks by category or project requirements."""  # noqa: E501
    request_data: Any = None
    if isinstance(request_data, dict):
        request_data = remove_none_values(request_data)
    content = json.dumps(request_data) if request_data else None
    response = await make_request(
        url="https://app.asana.com/api/1.0/tags/{tag_gid}/tasks".format(tag_gid=tag_global_id),  # noqa: UP032
        method="GET",
        params=remove_none_values({
            "opt_pretty": enable_pretty_output,
            "limit": results_per_page,
            "offset": pagination_offset,
            "opt_fields": optional_fields_to_include,
        }),
        headers=remove_none_values({
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            )
        }),
        content=content,
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(requires_auth=OAuth2(id="arcade-asana", scopes=["tasks:read"]))
async def get_user_task_list(
    context: ToolContext,
    user_task_list_id: Annotated[
        str,
        "Globally unique identifier for the user's task list. This ID is required to fetch tasks from the specified user list.",  # noqa: E501
    ],
    enable_pretty_output: Annotated[
        bool | None,
        "Enable pretty output to make the JSON response more readable. This increases response time and size, so it's recommended for debugging only.",  # noqa: E501
    ] = None,
    filter_completed_tasks_since: Annotated[
        str | None,
        "Return only tasks that are either incomplete or completed since this date-time or the keyword 'now'.",  # noqa: E501
    ] = None,
    include_optional_fields: Annotated[
        list[str] | None,
        "List the optional properties to include in the response, as a list of strings. These properties are excluded by default.",  # noqa: E501
    ] = None,
    pagination_offset_token: Annotated[
        str | None,
        "Offset token for pagination. Use the token returned from a previous request to fetch the next page. If not provided, the first page is returned.",  # noqa: E501
    ] = None,
    tasks_per_page: Annotated[
        int | None, "Specify the number of tasks to return per page, between 1 and 100."
    ] = None,
) -> Annotated[dict[str, Any], "Response from the API endpoint 'getTasksForUserTaskList'."]:
    """Retrieve tasks in a user's My Tasks list.

    Fetches a compact list of tasks for a user's My Tasks list from Asana. It includes both complete and incomplete tasks unless filtered. Access control applies, so private tasks are excluded if the authenticated user lacks permissions."""  # noqa: E501
    request_data: Any = None
    if isinstance(request_data, dict):
        request_data = remove_none_values(request_data)
    content = json.dumps(request_data) if request_data else None
    response = await make_request(
        url="https://app.asana.com/api/1.0/user_task_lists/{user_task_list_gid}/tasks".format(  # noqa: UP032
            user_task_list_gid=user_task_list_id
        ),
        method="GET",
        params=remove_none_values({
            "completed_since": filter_completed_tasks_since,
            "opt_pretty": enable_pretty_output,
            "limit": tasks_per_page,
            "offset": pagination_offset_token,
            "opt_fields": include_optional_fields,
        }),
        headers=remove_none_values({
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            )
        }),
        content=content,
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(requires_auth=OAuth2(id="arcade-asana", scopes=["tasks:read"]))
async def get_task_subtasks(
    context: ToolContext,
    task_id: Annotated[str, "The unique identifier of the task to retrieve subtasks for."],
    enable_pretty_output: Annotated[
        bool | None,
        "Enable to receive the response in a 'pretty' format with line breaks and indentation. Useful for debugging. May increase response time and size.",  # noqa: E501
    ] = None,
    include_optional_fields: Annotated[
        list[str] | None, "Comma-separated list of optional properties to include in the response."
    ] = None,
    pagination_offset: Annotated[
        str | None,
        "An offset token for pagination to retrieve the next page of results. Pass the token from a previous paginated request. If not provided, the first page is returned.",  # noqa: E501
    ] = None,
    results_per_page: Annotated[
        int | None, "Specify the number of subtasks to return per page. Must be between 1 and 100."
    ] = None,
) -> Annotated[dict[str, Any], "Response from the API endpoint 'getSubtasksForTask'."]:
    """Retrieve subtasks for a specific task in Asana.

    Use this tool to get a list of subtasks for a given task in Asana. Useful when you need to see all the subtasks associated with a task to track progress or manage workflow."""  # noqa: E501
    request_data: Any = None
    if isinstance(request_data, dict):
        request_data = remove_none_values(request_data)
    content = json.dumps(request_data) if request_data else None
    response = await make_request(
        url="https://app.asana.com/api/1.0/tasks/{task_gid}/subtasks".format(task_gid=task_id),  # noqa: UP032
        method="GET",
        params=remove_none_values({
            "opt_pretty": enable_pretty_output,
            "limit": results_per_page,
            "offset": pagination_offset,
            "opt_fields": include_optional_fields,
        }),
        headers=remove_none_values({
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            )
        }),
        content=content,
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(requires_auth=OAuth2(id="arcade-asana", scopes=["tasks:write"]))
async def create_subtask_for_task(
    context: ToolContext,
    mode: Annotated[
        ToolMode,
        "Operation mode: 'get_request_schema' returns the OpenAPI spec "
        "for the request body, 'execute' performs the actual operation",
    ],
    parent_task_id: Annotated[
        str | None,
        "The ID of the parent task for which the new subtask will be created.  Required when mode is 'execute', ignored when mode is 'get_request_schema'.",  # noqa: E501
    ] = None,
    included_optional_fields: Annotated[
        list[str] | None,
        "Comma-separated list of properties to include in the response. Allows access to optional fields not included by default.  Only used when mode is 'execute'.",  # noqa: E501
    ] = None,
    enable_pretty_output: Annotated[
        bool | None,
        "Set to true for a formatted JSON response with line breaks and indentation. Increases size and time; use for debugging.  Only used when mode is 'execute'.",  # noqa: E501
    ] = None,
    request_body: Annotated[
        str | None,
        "Stringified JSON representing the request body. Required when "
        "mode is 'execute', ignored when mode is 'get_request_schema'",
    ] = None,
) -> Annotated[dict[str, Any], "Response from the API endpoint 'createSubtaskForTask'."]:
    """Create a new subtask under a specified parent task.

    This tool is used to create a new subtask and associate it with an existing parent task in Asana. It requires the 'tasks:write' scope and returns the complete record of the newly created subtask.

    Note: Understanding the request schema is necessary to properly create
    the stringified JSON input object for execution.\n\nThis operation also requires path, query parameters.

    Modes:
    - GET_REQUEST_SCHEMA: Returns the schema. Only call if you don't
      already have it. Do NOT call repeatedly if you already received
      the schema.
    - EXECUTE: Performs the operation with the provided request body
      JSON.\n      Note: You must also provide the required path, query parameters when executing.

    If you need the schema, call with mode='get_request_schema' ONCE, then execute.
    """  # noqa: E501
    if mode == ToolMode.GET_REQUEST_SCHEMA:
        return {
            "request_body_schema": REQUEST_BODY_SCHEMAS["CREATESUBTASKFORTASK"],
            "instructions": (
                "Use the request_body_schema to construct a valid JSON object. "
                "Once you have populated the object following the schema "
                "structure and requirements, call this tool again with "
                "mode='execute' and the stringified JSON as the "
                "request_body parameter along with the required path, query parameters. "
                "Do NOT call the schema mode again - you already have "
                "the schema now."
            ),
        }

    # Mode is EXECUTE - validate parameters
    # Validate required parameters
    missing_params = []
    if not parent_task_id:
        missing_params.append(("parent_task_id", "path"))

    if missing_params:
        param_names = [p[0] for p in missing_params]
        param_details = ", ".join([f"{p[0]} ({p[1]})" for p in missing_params])
        raise RetryableToolError(
            message=f"Missing required parameters: {param_names}",
            developer_message=(f"Required parameters validation failed: {param_details}"),
            additional_prompt_content=(
                f"The following required parameters are missing: "
                f"{param_details}. Please call this tool again with all "
                "required parameters."
            ),
        )

    # Validate request body is provided (not None or empty string)
    # Note: Empty objects like {} are allowed - schema validation will check if valid
    if request_body is None or request_body.strip() == "":
        raise RetryableToolError(
            message="Request body is required when mode is 'execute'",
            developer_message="The request_body parameter was null or empty string",
            additional_prompt_content=(
                "The request body is required to perform this operation. "
                "Use the schema below to construct a valid JSON object, "
                "then call this tool again in execute mode with the "
                "stringified JSON as the request_body parameter.\n\n"
                "Schema:\n\n```json\n" + REQUEST_BODY_SCHEMAS["CREATESUBTASKFORTASK"] + "\n```"
            ),
        )

    # Parse JSON
    try:
        request_data = json.loads(request_body)
    except json.JSONDecodeError as e:
        raise RetryableToolError(
            message=f"Invalid JSON in request body: {e!s}",
            developer_message=f"JSON parsing failed: {e!s}",
            additional_prompt_content=(
                f"The request body contains invalid JSON. Error: {e!s}\n\n"
                "Please provide a valid JSON string that matches the schema "
                "below, then call this tool again in execute mode.\n\n"
                "Schema:\n\n```json\n" + REQUEST_BODY_SCHEMAS["CREATESUBTASKFORTASK"] + "\n```"
            ),
        ) from e

    response = await make_request_with_schema_validation(
        url="https://app.asana.com/api/1.0/tasks/{task_gid}/subtasks".format(  # noqa: UP032
            task_gid=parent_task_id
        ),
        method="POST",
        request_data=request_data,
        schema=REQUEST_BODY_SCHEMAS["CREATESUBTASKFORTASK"],
        params=remove_none_values({
            "opt_pretty": enable_pretty_output,
            "opt_fields": included_optional_fields,
        }),
        headers=remove_none_values({
            "Content-Type": "application/json",
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            ),
        }),
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(requires_auth=OAuth2(id="arcade-asana", scopes=["tasks:write"]))
async def update_task_parent_asana(
    context: ToolContext,
    task_identifier: Annotated[
        str, "The unique identifier of the task to operate on. It should be a string."
    ],
    enable_pretty_output: Annotated[
        bool | None,
        "Set this to true to receive formatted output with line breaks and indentation. This is useful for debugging but increases response size and time.",  # noqa: E501
    ] = None,
    include_optional_properties: Annotated[
        list[str] | None,
        "A list of optional task properties to include in the response. Use a list of property names.",  # noqa: E501
    ] = None,
    new_task_parent: Annotated[
        str | None, "The new parent task ID for the task, or null to remove its current parent."
    ] = None,
    subtask_to_insert_after: Annotated[
        str | None,
        "Specify the subtask ID to insert the task after, or use 'null' to insert at the beginning of the list.",  # noqa: E501
    ] = None,
    subtask_to_insert_before: Annotated[
        str | None,
        "Specify a subtask ID to insert the task before, or null to place at the end of the list.",
    ] = None,
) -> Annotated[dict[str, Any], "Response from the API endpoint 'setParentForTask'."]:
    """Update the parent of an Asana task.

    This tool updates the parent of a given task in Asana, allowing it to become a subtask of another task or to remove its existing parent. Utilize `insert_before` or `insert_after` to position subtasks appropriately, ensuring they are already part of the parent task. Requires 'tasks:write' scope."""  # noqa: E501
    request_data: Any = {
        "data": {
            "parent": new_task_parent,
            "insert_after": subtask_to_insert_after,
            "insert_before": subtask_to_insert_before,
        }
    }
    if isinstance(request_data, dict):
        request_data = remove_none_values(request_data)
    content = json.dumps(request_data) if request_data else None
    response = await make_request(
        url="https://app.asana.com/api/1.0/tasks/{task_gid}/setParent".format(  # noqa: UP032
            task_gid=task_identifier
        ),
        method="POST",
        params=remove_none_values({
            "opt_pretty": enable_pretty_output,
            "opt_fields": include_optional_properties,
        }),
        headers=remove_none_values({
            "Content-Type": "application/json",
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            ),
        }),
        content=content,
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(requires_auth=OAuth2(id="arcade-asana", scopes=["tasks:read"]))
async def get_task_dependencies(
    context: ToolContext,
    task_id: Annotated[str, "The global ID of the task to retrieve dependencies for in Asana."],
    enable_pretty_output: Annotated[
        bool | None,
        "Enable pretty formatting for output; useful for debugging. Increases response size and time.",  # noqa: E501
    ] = None,
    included_fields: Annotated[
        list[str] | None,
        "Comma-separated list of properties to include in the response. Some properties are excluded by default.",  # noqa: E501
    ] = None,
    pagination_offset_token: Annotated[
        str | None,
        "The offset token for pagination, used to retrieve the next page of results. Pass this only if paginating a previous request.",  # noqa: E501
    ] = None,
    results_per_page: Annotated[
        int | None,
        "Specify the number of task dependencies to return per page. Must be between 1 and 100.",
    ] = None,
) -> Annotated[dict[str, Any], "Response from the API endpoint 'getDependenciesForTask'."]:
    """Retrieve all dependencies for a specific Asana task.

    This tool is used to get all dependencies associated with a specific task in Asana. It should be utilized when there's a need to understand the dependencies that a task has, requiring tasks:read scope."""  # noqa: E501
    request_data: Any = None
    if isinstance(request_data, dict):
        request_data = remove_none_values(request_data)
    content = json.dumps(request_data) if request_data else None
    response = await make_request(
        url="https://app.asana.com/api/1.0/tasks/{task_gid}/dependencies".format(task_gid=task_id),  # noqa: UP032
        method="GET",
        params=remove_none_values({
            "opt_pretty": enable_pretty_output,
            "limit": results_per_page,
            "offset": pagination_offset_token,
            "opt_fields": included_fields,
        }),
        headers=remove_none_values({
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            )
        }),
        content=content,
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(requires_auth=OAuth2(id="arcade-asana", scopes=["tasks:write"]))
async def add_task_dependencies(
    context: ToolContext,
    task_id_to_modify: Annotated[
        str, "The unique identifier of the task to which dependencies will be added."
    ],
    enable_pretty_output: Annotated[
        bool | None,
        "Enable pretty formatting for the response output. Provides improved readability with line breaking and indentation, recommended for debugging only.",  # noqa: E501
    ] = None,
    task_dependency_ids: Annotated[
        list[str] | None,
        "An array of task GIDs that the current task depends on. These are required to establish dependencies between tasks in Asana.",  # noqa: E501
    ] = None,
) -> Annotated[dict[str, Any], "Response from the API endpoint 'addDependenciesForTask'."]:
    """Add dependencies to an Asana task.

    Marks specified tasks as dependencies for a given task in Asana, ensuring they aren't already marked. Suitable for managing task workflows where completion order is essential."""  # noqa: E501
    request_data: Any = {"data": {"dependencies": task_dependency_ids}}
    if isinstance(request_data, dict):
        request_data = remove_none_values(request_data)
    content = json.dumps(request_data) if request_data else None
    response = await make_request(
        url="https://app.asana.com/api/1.0/tasks/{task_gid}/addDependencies".format(  # noqa: UP032
            task_gid=task_id_to_modify
        ),
        method="POST",
        params=remove_none_values({"opt_pretty": enable_pretty_output}),
        headers=remove_none_values({
            "Content-Type": "application/json",
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            ),
        }),
        content=content,
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(requires_auth=OAuth2(id="arcade-asana", scopes=["tasks:write"]))
async def remove_task_dependencies(
    context: ToolContext,
    task_id: Annotated[
        str, "The unique identifier of the task to operate on for removing dependencies."
    ],
    enable_pretty_output: Annotated[
        bool | None,
        "Set to true to receive a formatted, readable response. Useful for debugging, but increases response size and time.",  # noqa: E501
    ] = None,
    task_dependency_ids_to_remove: Annotated[
        list[str] | None,
        "An array of task GIDs representing dependencies to be removed from the specified task.",
    ] = None,
) -> Annotated[dict[str, Any], "Response from the API endpoint 'removeDependenciesForTask'."]:
    """Unlink dependencies from a specified task on Asana.

    Use this tool to remove a set of dependencies from a given task in Asana. Requires 'tasks:write' access scope."""  # noqa: E501
    request_data: Any = {"data": {"dependencies": task_dependency_ids_to_remove}}
    if isinstance(request_data, dict):
        request_data = remove_none_values(request_data)
    content = json.dumps(request_data) if request_data else None
    response = await make_request(
        url="https://app.asana.com/api/1.0/tasks/{task_gid}/removeDependencies".format(  # noqa: UP032
            task_gid=task_id
        ),
        method="POST",
        params=remove_none_values({"opt_pretty": enable_pretty_output}),
        headers=remove_none_values({
            "Content-Type": "application/json",
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            ),
        }),
        content=content,
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(requires_auth=OAuth2(id="arcade-asana", scopes=["tasks:read"]))
async def get_task_dependents(
    context: ToolContext,
    task_identifier: Annotated[
        str, "The unique identifier of the task to retrieve dependents for."
    ],
    enable_pretty_output: Annotated[
        bool | None,
        "Enable this for JSON responses with readable line breaks and indentation. Use mainly for debugging.",  # noqa: E501
    ] = None,
    include_optional_fields: Annotated[
        list[str] | None,
        "A list of optional properties to include in the response, specified as strings.",
    ] = None,
    pagination_offset: Annotated[
        str | None,
        "Offset token for paginated results. Use a token from a previous response to continue fetching results. Defaults to the first page if not provided.",  # noqa: E501
    ] = None,
    results_per_page: Annotated[
        int | None, "Number of results to return per page, between 1 and 100."
    ] = None,
) -> Annotated[dict[str, Any], "Response from the API endpoint 'getDependentsForTask'."]:
    """Retrieve the dependents of a specific task.

    Use this tool to obtain a list of all tasks dependent on a specified task in Asana. This tool requires the 'tasks:read' scope."""  # noqa: E501
    request_data: Any = None
    if isinstance(request_data, dict):
        request_data = remove_none_values(request_data)
    content = json.dumps(request_data) if request_data else None
    response = await make_request(
        url="https://app.asana.com/api/1.0/tasks/{task_gid}/dependents".format(  # noqa: UP032
            task_gid=task_identifier
        ),
        method="GET",
        params=remove_none_values({
            "opt_pretty": enable_pretty_output,
            "limit": results_per_page,
            "offset": pagination_offset,
            "opt_fields": include_optional_fields,
        }),
        headers=remove_none_values({
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            )
        }),
        content=content,
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(requires_auth=OAuth2(id="arcade-asana", scopes=["tasks:write"]))
async def add_dependents_to_task(
    context: ToolContext,
    target_task_id: Annotated[str, "The unique identifier for the task to add dependents to."],
    dependent_task_gids: Annotated[
        list[str] | None, "An array of task GIDs to be marked as dependents for this task."
    ] = None,
    enable_pretty_output: Annotated[
        bool | None,
        "Set to true to format the response for readability. Use during debugging, as it increases response time and size.",  # noqa: E501
    ] = None,
) -> Annotated[dict[str, Any], "Response from the API endpoint 'addDependentsForTask'."]:
    """Add dependents to an Asana task.

    Use this tool to mark a set of Asana tasks as dependents of another task, ensuring they are not already listed. Suitable for cases where task dependencies are being managed and a task can have up to 30 dependents and dependencies combined."""  # noqa: E501
    request_data: Any = {"data": {"dependents": dependent_task_gids}}
    if isinstance(request_data, dict):
        request_data = remove_none_values(request_data)
    content = json.dumps(request_data) if request_data else None
    response = await make_request(
        url="https://app.asana.com/api/1.0/tasks/{task_gid}/addDependents".format(  # noqa: UP032
            task_gid=target_task_id
        ),
        method="POST",
        params=remove_none_values({"opt_pretty": enable_pretty_output}),
        headers=remove_none_values({
            "Content-Type": "application/json",
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            ),
        }),
        content=content,
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(requires_auth=OAuth2(id="arcade-asana", scopes=["tasks:write"]))
async def remove_task_dependents(
    context: ToolContext,
    task_id: Annotated[str, "The unique identifier of the task to operate on."],
    dependents_task_ids: Annotated[
        list[str] | None, "An array of task GIDs that should be unlinked from the specified task."
    ] = None,
    enable_pretty_output: Annotated[
        bool | None,
        "Set to true for prettified JSON output. Recommended only for debugging as it increases response size and time.",  # noqa: E501
    ] = None,
) -> Annotated[dict[str, Any], "Response from the API endpoint 'removeDependentsForTask'."]:
    """Unlink dependents from an Asana task.

    Use this tool to unlink a set of dependents from a specific task in Asana. This is useful when you need to adjust task dependencies within your project management workflow."""  # noqa: E501
    request_data: Any = {"data": {"dependents": dependents_task_ids}}
    if isinstance(request_data, dict):
        request_data = remove_none_values(request_data)
    content = json.dumps(request_data) if request_data else None
    response = await make_request(
        url="https://app.asana.com/api/1.0/tasks/{task_gid}/removeDependents".format(  # noqa: UP032
            task_gid=task_id
        ),
        method="POST",
        params=remove_none_values({"opt_pretty": enable_pretty_output}),
        headers=remove_none_values({
            "Content-Type": "application/json",
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            ),
        }),
        content=content,
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(requires_auth=OAuth2(id="arcade-asana", scopes=["tasks:write"]))
async def add_task_to_project(
    context: ToolContext,
    task_global_id: Annotated[str, "The unique global ID of the task to be operated on."],
    enable_pretty_output: Annotated[
        bool | None,
        "Set to true for formatted JSON output, making it more readable. Useful for debugging. Note: This increases response time and size.",  # noqa: E501
    ] = None,
    insert_after_task_id: Annotated[
        str | None,
        "Provide the ID of a task in the project to insert this task after, or use 'null' to insert at the beginning.",  # noqa: E501
    ] = None,
    insert_task_before: Annotated[
        str | None,
        "Specify a task ID to insert the new task before it in the project, or use `null` to insert at the end of the list.",  # noqa: E501
    ] = None,
    project_id_to_add_task: Annotated[
        str | None, "The unique identifier of the project to which the task will be added."
    ] = None,
    target_section_id: Annotated[
        str | None, "The ID of the section in the project to insert the task at the bottom."
    ] = None,
) -> Annotated[dict[str, Any], "Response from the API endpoint 'addProjectForTask'."]:
    """Add a task to a specified Asana project.

    Use this to add a task to a specific project within Asana, optionally specifying the location. It can also reorder a task within a project. Only one of `insert_before`, `insert_after`, or `section` should be used to specify the location. Tasks can be associated with up to 20 projects."""  # noqa: E501
    request_data: Any = {
        "data": {
            "project": project_id_to_add_task,
            "insert_after": insert_after_task_id,
            "insert_before": insert_task_before,
            "section": target_section_id,
        }
    }
    if isinstance(request_data, dict):
        request_data = remove_none_values(request_data)
    content = json.dumps(request_data) if request_data else None
    response = await make_request(
        url="https://app.asana.com/api/1.0/tasks/{task_gid}/addProject".format(  # noqa: UP032
            task_gid=task_global_id
        ),
        method="POST",
        params=remove_none_values({"opt_pretty": enable_pretty_output}),
        headers=remove_none_values({
            "Content-Type": "application/json",
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            ),
        }),
        content=content,
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(requires_auth=OAuth2(id="arcade-asana", scopes=["tasks:write"]))
async def remove_task_from_project(
    context: ToolContext,
    task_identifier: Annotated[
        str, "The unique identifier of the task to remove from the project."
    ],
    enable_pretty_output: Annotated[
        bool | None,
        "Enable 'pretty' response formatting. Provides line breaks and indentation for readability, useful mainly for debugging.",  # noqa: E501
    ] = None,
    project_to_remove_task_from: Annotated[
        str | None, "The identifier of the project from which the task will be removed."
    ] = None,
) -> Annotated[dict[str, Any], "Response from the API endpoint 'removeProjectForTask'."]:
    """Remove a task from the specified project in Asana.

    Removes a task from a specified project within Asana. The task continues to exist in the system but will no longer be associated with the project. Requires 'tasks:write' scope."""  # noqa: E501
    request_data: Any = {"data": {"project": project_to_remove_task_from}}
    if isinstance(request_data, dict):
        request_data = remove_none_values(request_data)
    content = json.dumps(request_data) if request_data else None
    response = await make_request(
        url="https://app.asana.com/api/1.0/tasks/{task_gid}/removeProject".format(  # noqa: UP032
            task_gid=task_identifier
        ),
        method="POST",
        params=remove_none_values({"opt_pretty": enable_pretty_output}),
        headers=remove_none_values({
            "Content-Type": "application/json",
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            ),
        }),
        content=content,
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(requires_auth=OAuth2(id="arcade-asana", scopes=["tasks:write"]))
async def add_tag_to_task(
    context: ToolContext,
    task_id: Annotated[str, "The unique identifier of the task to which the tag should be added."],
    enable_pretty_output: Annotated[
        bool | None,
        "Enable this to receive a prettified JSON response. Useful for debugging, but increases response time and size.",  # noqa: E501
    ] = None,
    tag_gid_to_add: Annotated[
        str | None, "The GID of the tag to be added to the specified task."
    ] = None,
) -> Annotated[dict[str, Any], "Response from the API endpoint 'addTagForTask'."]:
    """Add a tag to a specific Asana task.

    This tool adds a tag to a specified task in Asana. It should be used when you need to organize tasks by attaching tags. Requires 'tasks:write' scope."""  # noqa: E501
    request_data: Any = {"data": {"tag": tag_gid_to_add}}
    if isinstance(request_data, dict):
        request_data = remove_none_values(request_data)
    content = json.dumps(request_data) if request_data else None
    response = await make_request(
        url="https://app.asana.com/api/1.0/tasks/{task_gid}/addTag".format(task_gid=task_id),  # noqa: UP032
        method="POST",
        params=remove_none_values({"opt_pretty": enable_pretty_output}),
        headers=remove_none_values({
            "Content-Type": "application/json",
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            ),
        }),
        content=content,
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(requires_auth=OAuth2(id="arcade-asana", scopes=["tasks:write"]))
async def remove_tag_from_task(
    context: ToolContext,
    task_identifier: Annotated[
        str, "The unique identifier of the task from which the tag should be removed."
    ],
    enable_pretty_output: Annotated[
        bool | None,
        "If true, formats the response for readability with line breaks and indentation. Use primarily for debugging.",  # noqa: E501
    ] = None,
    tag_gid_to_remove: Annotated[
        str | None, "The GID of the tag to be removed from the task in Asana."
    ] = None,
) -> Annotated[dict[str, Any], "Response from the API endpoint 'removeTagForTask'."]:
    """Remove a tag from a task in Asana.

    Use this tool to remove a specific tag from a task in Asana. This action requires write access to tasks."""  # noqa: E501
    request_data: Any = {"data": {"tag": tag_gid_to_remove}}
    if isinstance(request_data, dict):
        request_data = remove_none_values(request_data)
    content = json.dumps(request_data) if request_data else None
    response = await make_request(
        url="https://app.asana.com/api/1.0/tasks/{task_gid}/removeTag".format(  # noqa: UP032
            task_gid=task_identifier
        ),
        method="POST",
        params=remove_none_values({"opt_pretty": enable_pretty_output}),
        headers=remove_none_values({
            "Content-Type": "application/json",
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            ),
        }),
        content=content,
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(requires_auth=OAuth2(id="arcade-asana", scopes=["tasks:write"]))
async def add_followers_to_task(
    context: ToolContext,
    task_gid: Annotated[
        str,
        "The unique identifier of the task to add followers to. This is required to specify which task the followers should be added to.",  # noqa: E501
    ],
    enable_pretty_output: Annotated[
        bool | None,
        "Enables pretty formatting for the response, adding line breaks and indentation. Useful for debugging but increases response size.",  # noqa: E501
    ] = None,
    followers_identification: Annotated[
        list[str] | None,
        "An array of strings identifying users, which can be 'me', an email, or a user gid.",
    ] = None,
    include_optional_properties: Annotated[
        list[str] | None,
        "Comma-separated list of optional task properties to include in the response.",
    ] = None,
) -> Annotated[dict[str, Any], "Response from the API endpoint 'addFollowersForTask'."]:
    """Adds followers to an Asana task.

    Use this tool to add one or more followers to a specific task in Asana. The request will return the updated task record, reflecting the changes."""  # noqa: E501
    request_data: Any = {"data": {"followers": followers_identification}}
    if isinstance(request_data, dict):
        request_data = remove_none_values(request_data)
    content = json.dumps(request_data) if request_data else None
    response = await make_request(
        url="https://app.asana.com/api/1.0/tasks/{task_gid}/addFollowers".format(task_gid=task_gid),  # noqa: UP032
        method="POST",
        params=remove_none_values({
            "opt_pretty": enable_pretty_output,
            "opt_fields": include_optional_properties,
        }),
        headers=remove_none_values({
            "Content-Type": "application/json",
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            ),
        }),
        content=content,
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(requires_auth=OAuth2(id="arcade-asana", scopes=["tasks:write"]))
async def asana_remove_follower_for_task(
    context: ToolContext,
    task_id: Annotated[
        str, "The unique identifier of the task from which followers are to be removed."
    ],
    enable_pretty_output: Annotated[
        bool | None,
        "Set to true for readable JSON with line breaks and indentation. Use for debugging; increases response size and processing time.",  # noqa: E501
    ] = None,
    followers_to_remove: Annotated[
        list[str] | None,
        'An array of strings identifying users to remove as followers. Acceptable formats: "me", an email, or a user\'s gid.',  # noqa: E501
    ] = None,
    include_optional_properties: Annotated[
        list[str] | None,
        "A list of optional task properties to include in the response. Specify as an array of strings.",  # noqa: E501
    ] = None,
) -> Annotated[dict[str, Any], "Response from the API endpoint 'removeFollowerForTask'."]:
    """Remove followers from an Asana task.

    This tool removes specified followers from a given Asana task and returns the complete, updated task record. It should be called when you need to update a task by removing certain users as followers. Requires 'tasks:write' scope."""  # noqa: E501
    request_data: Any = {"data": {"followers": followers_to_remove}}
    if isinstance(request_data, dict):
        request_data = remove_none_values(request_data)
    content = json.dumps(request_data) if request_data else None
    response = await make_request(
        url="https://app.asana.com/api/1.0/tasks/{task_gid}/removeFollowers".format(  # noqa: UP032
            task_gid=task_id
        ),
        method="POST",
        params=remove_none_values({
            "opt_pretty": enable_pretty_output,
            "opt_fields": include_optional_properties,
        }),
        headers=remove_none_values({
            "Content-Type": "application/json",
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            ),
        }),
        content=content,
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(requires_auth=OAuth2(id="arcade-asana", scopes=["tasks:read"]))
async def retrieve_task_by_custom_id(
    context: ToolContext,
    task_custom_id: Annotated[
        str, "The generated custom ID used to identify a specific task in Asana."
    ],
    workspace_global_id: Annotated[
        str, "Globally unique identifier for the workspace or organization in Asana."
    ],
) -> Annotated[dict[str, Any], "Response from the API endpoint 'getTaskForCustomID'."]:
    """Retrieve a task using a custom ID in Asana.

    This tool retrieves details of a task from Asana using a given custom ID shortcode. It requires the necessary scope for accessing task details and may include additional scopes to access related memberships or time tracking information."""  # noqa: E501
    request_data: Any = None
    if isinstance(request_data, dict):
        request_data = remove_none_values(request_data)
    content = json.dumps(request_data) if request_data else None
    response = await make_request(
        url="https://app.asana.com/api/1.0/workspaces/{workspace_gid}/tasks/custom_id/{custom_id}".format(  # noqa: UP032
            workspace_gid=workspace_global_id, custom_id=task_custom_id
        ),
        method="GET",
        params=remove_none_values({}),
        headers=remove_none_values({
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            )
        }),
        content=content,
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(requires_auth=OAuth2(id="arcade-asana", scopes=["tasks:read"]))
async def search_tasks_in_workspace(
    context: ToolContext,
    workspace_global_id: Annotated[
        str,
        "Globally unique identifier for the workspace or organization where tasks are searched.",
    ],
    all_tags_filters: Annotated[
        str | None, "Comma-separated list of tag IDs to filter tasks that have all specified tags."
    ] = None,
    assigned_by_user_ids: Annotated[
        str | None, "A comma-separated list of user IDs to filter tasks assigned by specific users."
    ] = None,
    assignee_identifiers_any: Annotated[
        str | None,
        "Provide a comma-separated list of user identifiers for any assignee to include in the search.",  # noqa: E501
    ] = None,
    before_modified_on_date: Annotated[
        str | None, "ISO 8601 date string to filter tasks modified before this date."
    ] = None,
    completed_at_after: Annotated[
        str | None,
        "Specify an ISO 8601 datetime string to filter tasks completed after this date and time.",
    ] = None,
    completed_at_before_datetime: Annotated[
        str | None, "Filter tasks completed before the specified ISO 8601 datetime."
    ] = None,
    completed_on_after_date: Annotated[
        str | None, "Specify an ISO 8601 date to filter tasks completed after this date."
    ] = None,
    completed_on_before_date: Annotated[
        str | None, "Filter tasks completed before a specific date using an ISO 8601 date string."
    ] = None,
    completed_on_date: Annotated[
        str | None,
        "Filter tasks by their completion date using an ISO 8601 date string or `null` for no specific date.",  # noqa: E501
    ] = None,
    created_after_date: Annotated[
        str | None,
        "Specify the earliest creation date for tasks using an ISO 8601 date string. Filters tasks created after this date.",  # noqa: E501
    ] = None,
    created_after_datetime: Annotated[
        str | None, "Filter tasks created after this ISO 8601 datetime string."
    ] = None,
    created_at_before: Annotated[
        str | None, "An ISO 8601 datetime string to filter tasks created before this date and time."
    ] = None,
    created_by_users: Annotated[
        str | None, "Comma-separated list of user IDs to filter tasks created by specific users."
    ] = None,
    created_on_before_date: Annotated[
        str | None,
        "Filter tasks created before a specific date using an ISO 8601 date string format.",
    ] = None,
    created_on_date: Annotated[
        str | None, "Filter tasks by their creation date using an ISO 8601 date string or `null`."
    ] = None,
    due_at_after_datetime: Annotated[
        str | None,
        "Specify the start date and time for tasks due after this point. Use an ISO 8601 datetime string.",  # noqa: E501
    ] = None,
    due_at_before: Annotated[
        str | None, "An ISO 8601 datetime string to filter tasks due before this date and time."
    ] = None,
    due_date_on: Annotated[
        str | None,
        "Filter tasks due on a specific date using an ISO 8601 date string or specify `null` for tasks with no due date.",  # noqa: E501
    ] = None,
    enable_pretty_output: Annotated[
        bool | None,
        "Formats the response with line breaks and indentation for readability. Use for debugging, as it increases response size and processing time.",  # noqa: E501
    ] = None,
    end_date_before_start_on: Annotated[
        str | None, "Filter tasks starting before a specified date, using an ISO 8601 date string."
    ] = None,
    exclude_assigned_by_users: Annotated[
        str | None, "A comma-separated list of user IDs to exclude tasks assigned by these users."
    ] = None,
    exclude_assignees: Annotated[
        str | None, "Comma-separated list of user identifiers to exclude from the search results."
    ] = None,
    exclude_created_by_user_ids: Annotated[
        str | None, "Comma-separated list of user IDs to exclude from the search results."
    ] = None,
    exclude_followers_by_user_ids: Annotated[
        str | None, "Comma-separated list of user identifiers to exclude from the followers filter."
    ] = None,
    exclude_projects_by_id: Annotated[
        str | None, "A comma-separated list of project IDs to exclude from the search results."
    ] = None,
    exclude_sections_by_id: Annotated[
        str | None,
        "Comma-separated list of section or column IDs to exclude from the search results.",
    ] = None,
    exclude_tags_by_ids: Annotated[
        str | None, "A comma-separated list of tag IDs to exclude from the search results."
    ] = None,
    exclude_tasks_commented_by_users: Annotated[
        str | None,
        "Comma-separated list of user identifiers to exclude tasks commented on by these users.",
    ] = None,
    excluded_liked_by_user_ids: Annotated[
        str | None, "A comma-separated list of user IDs to exclude tasks liked by these users."
    ] = None,
    filter_by_all_projects: Annotated[
        str | None,
        "Comma-separated list of project IDs to filter tasks that belong to all specified projects.",  # noqa: E501
    ] = None,
    filter_by_any_follower_ids: Annotated[
        str | None,
        "Filter tasks by providing a comma-separated list of user IDs who are followers of the tasks.",  # noqa: E501
    ] = None,
    filter_by_any_team_ids: Annotated[
        str | None,
        "Comma-separated list of team IDs to filter tasks associated with any of these teams.",
    ] = None,
    filter_due_date_before: Annotated[
        str | None, "Specify tasks with a due date earlier than this ISO 8601 date string."
    ] = None,
    filter_modified_date_start: Annotated[
        str | None, "Start date to filter tasks modified after this date in ISO 8601 format."
    ] = None,
    filter_sections_all: Annotated[
        str | None,
        "Comma-separated list of section or column IDs to filter tasks by inclusion in all specified sections.",  # noqa: E501
    ] = None,
    filter_tasks_with_attachments: Annotated[
        bool | None,
        "Set to true to filter tasks that have attachments, and false to include all tasks regardless of attachments.",  # noqa: E501
    ] = None,
    filter_tasks_with_incomplete_dependencies: Annotated[
        bool | None,
        "Filter tasks to those with incomplete dependencies. Use true to apply the filter.",
    ] = None,
    filter_to_completed_tasks: Annotated[
        bool | None, "Set to true to filter and display only completed tasks."
    ] = None,
    filter_to_incomplete_tasks_with_dependents: Annotated[
        bool | None, "Set to true to filter tasks to those that are incomplete and have dependents."
    ] = None,
    include_any_tags_ids: Annotated[
        str | None, "Comma-separated list of tag IDs to include in the search filter."
    ] = None,
    include_only_subtasks: Annotated[
        bool | None,
        "Set to true to include only subtasks in the results, false to include all tasks.",
    ] = None,
    include_sections_in_search: Annotated[
        str | None, "A comma-separated list of section or column IDs to filter tasks in the search."
    ] = None,
    included_optional_properties: Annotated[
        list[str] | None,
        "List of optional properties to include for each returned task. Provide as comma-separated values.",  # noqa: E501
    ] = None,
    last_modified_on: Annotated[
        str | None, "ISO 8601 date string or `null` to filter tasks based on the modification date."
    ] = None,
    modified_after_datetime: Annotated[
        str | None, "Filter tasks modified after this ISO 8601 datetime string."
    ] = None,
    modified_at_before_date: Annotated[
        str | None, "Filter tasks modified before this date. Use ISO 8601 datetime format."
    ] = None,
    portfolio_ids_included: Annotated[
        str | None, "A comma-separated list of portfolio IDs to include in the search."
    ] = None,
    project_ids_any: Annotated[
        str | None, "Comma-separated list of project IDs to include in the search."
    ] = None,
    search_text: Annotated[
        str | None, "Full-text search on task names and descriptions within the workspace."
    ] = None,
    sort_results_ascending: Annotated[
        bool | None, "Set to true to sort search results in ascending order. Default is false."
    ] = False,
    start_date_after: Annotated[
        str | None, "An ISO 8601 date string to filter tasks starting after this date."
    ] = None,
    start_on_date: Annotated[
        str | None,
        "ISO 8601 date string specifying the start date of tasks to be searched, or `null` for unspecified.",  # noqa: E501
    ] = None,
    task_resource_subtype: Annotated[
        str | None,
        "Filter tasks by their resource subtype, such as 'default_task', 'milestone', or 'approval'.",  # noqa: E501
    ] = "milestone",
    task_sort_order: Annotated[
        str | None,
        "Specify the sorting criteria for the task results. Options include 'due_date', 'created_at', 'completed_at', 'likes', or 'modified_at'. Defaults to 'modified_at'.",  # noqa: E501
    ] = "modified_at",
    tasks_due_after_date: Annotated[
        str | None, "Specify an ISO 8601 date string to filter tasks due after this date."
    ] = None,
) -> Annotated[dict[str, Any], "Response from the API endpoint 'searchTasksForWorkspace'."]:
    """Search for tasks in an Asana workspace using complex filters.

    Utilize this tool to perform an advanced search for tasks within a specified Asana workspace. It supports complex filters and is available to premium Asana users. Use it to find tasks with specific criteria, including custom field parameters. Note that pagination needs to be handled manually due to search result variability."""  # noqa: E501
    request_data: Any = None
    if isinstance(request_data, dict):
        request_data = remove_none_values(request_data)
    content = json.dumps(request_data) if request_data else None
    response = await make_request(
        url="https://app.asana.com/api/1.0/workspaces/{workspace_gid}/tasks/search".format(  # noqa: UP032
            workspace_gid=workspace_global_id
        ),
        method="GET",
        params=remove_none_values({
            "opt_pretty": enable_pretty_output,
            "text": search_text,
            "resource_subtype": task_resource_subtype,
            "assignee.any": assignee_identifiers_any,
            "assignee.not": exclude_assignees,
            "portfolios.any": portfolio_ids_included,
            "projects.any": project_ids_any,
            "projects.not": exclude_projects_by_id,
            "projects.all": filter_by_all_projects,
            "sections.any": include_sections_in_search,
            "sections.not": exclude_sections_by_id,
            "sections.all": filter_sections_all,
            "tags.any": include_any_tags_ids,
            "tags.not": exclude_tags_by_ids,
            "tags.all": all_tags_filters,
            "teams.any": filter_by_any_team_ids,
            "followers.any": filter_by_any_follower_ids,
            "followers.not": exclude_followers_by_user_ids,
            "created_by.any": created_by_users,
            "created_by.not": exclude_created_by_user_ids,
            "assigned_by.any": assigned_by_user_ids,
            "assigned_by.not": exclude_assigned_by_users,
            "liked_by.not": excluded_liked_by_user_ids,
            "commented_on_by.not": exclude_tasks_commented_by_users,
            "due_on.before": filter_due_date_before,
            "due_on.after": tasks_due_after_date,
            "due_on": due_date_on,
            "due_at.before": due_at_before,
            "due_at.after": due_at_after_datetime,
            "start_on.before": end_date_before_start_on,
            "start_on.after": start_date_after,
            "start_on": start_on_date,
            "created_on.before": created_on_before_date,
            "created_on.after": created_after_date,
            "created_on": created_on_date,
            "created_at.before": created_at_before,
            "created_at.after": created_after_datetime,
            "completed_on.before": completed_on_before_date,
            "completed_on.after": completed_on_after_date,
            "completed_on": completed_on_date,
            "completed_at.before": completed_at_before_datetime,
            "completed_at.after": completed_at_after,
            "modified_on.before": before_modified_on_date,
            "modified_on.after": filter_modified_date_start,
            "modified_on": last_modified_on,
            "modified_at.before": modified_at_before_date,
            "modified_at.after": modified_after_datetime,
            "is_blocking": filter_to_incomplete_tasks_with_dependents,
            "is_blocked": filter_tasks_with_incomplete_dependencies,
            "has_attachment": filter_tasks_with_attachments,
            "completed": filter_to_completed_tasks,
            "is_subtask": include_only_subtasks,
            "sort_by": task_sort_order,
            "sort_ascending": sort_results_ascending,
            "opt_fields": included_optional_properties,
        }),
        headers=remove_none_values({
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            )
        }),
        content=content,
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(requires_auth=OAuth2(id="arcade-asana", scopes=["team_memberships:read"]))
async def get_team_membership(
    context: ToolContext,
    team_membership_id: Annotated[
        str, "The unique identifier for the specific team membership to retrieve."
    ],
    enable_pretty_output: Annotated[
        bool | None,
        "Enable pretty formatting for the response with line breaks and indentation. Useful for debugging, but increases response time and size.",  # noqa: E501
    ] = None,
    include_optional_properties: Annotated[
        list[str] | None,
        "A list of optional properties to include in the response, specified as strings.",
    ] = None,
) -> Annotated[dict[str, Any], "Response from the API endpoint 'getTeamMembership'."]:
    """Retrieve complete details for a team membership.

    Use this tool to obtain the full team membership record for a specific team membership in Asana. It requires the 'team_memberships:read' scope."""  # noqa: E501
    request_data: Any = None
    if isinstance(request_data, dict):
        request_data = remove_none_values(request_data)
    content = json.dumps(request_data) if request_data else None
    response = await make_request(
        url="https://app.asana.com/api/1.0/team_memberships/{team_membership_gid}".format(  # noqa: UP032
            team_membership_gid=team_membership_id
        ),
        method="GET",
        params=remove_none_values({
            "opt_pretty": enable_pretty_output,
            "opt_fields": include_optional_properties,
        }),
        headers=remove_none_values({
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            )
        }),
        content=content,
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(requires_auth=OAuth2(id="arcade-asana", scopes=["team_memberships:read"]))
async def get_team_memberships(
    context: ToolContext,
    enable_pretty_output: Annotated[
        bool | None,
        "Return response in a readable format with line breaks and indentation. Ideal for debugging.",  # noqa: E501
    ] = None,
    optional_fields_to_include: Annotated[
        list[str] | None,
        "List the properties to include in the response, as some are excluded by default.",
    ] = None,
    pagination_offset_token: Annotated[
        str | None,
        "An offset token for pagination. Use a previously returned token to access subsequent pages. If omitted, the first page is returned.",  # noqa: E501
    ] = None,
    results_per_page: Annotated[
        int | None, "Specify the number of results to return per page. Must be between 1 and 100."
    ] = None,
    team_identifier: Annotated[
        str | None, "Globally unique identifier for the team in Asana."
    ] = None,
    user_identifier: Annotated[
        str | None,
        "A string identifying a user. Use 'me', an email, or a user gid. Must be used with 'workspace'.",  # noqa: E501
    ] = None,
    workspace_id: Annotated[
        str | None,
        "Globally unique identifier for the workspace. This parameter must be used with the user_id parameter.",  # noqa: E501
    ] = None,
) -> Annotated[dict[str, Any], "Response from the API endpoint 'getTeamMemberships'."]:
    """Retrieve compact team membership records from Asana.

    Fetches compact records of team memberships from Asana when scope 'team_memberships:read' is required."""  # noqa: E501
    request_data: Any = None
    if isinstance(request_data, dict):
        request_data = remove_none_values(request_data)
    content = json.dumps(request_data) if request_data else None
    response = await make_request(
        url="https://app.asana.com/api/1.0/team_memberships",
        method="GET",
        params=remove_none_values({
            "opt_pretty": enable_pretty_output,
            "limit": results_per_page,
            "offset": pagination_offset_token,
            "team": team_identifier,
            "user": user_identifier,
            "workspace": workspace_id,
            "opt_fields": optional_fields_to_include,
        }),
        headers=remove_none_values({
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            )
        }),
        content=content,
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(requires_auth=OAuth2(id="arcade-asana", scopes=["team_memberships:read"]))
async def fetch_team_members(
    context: ToolContext,
    team_global_identifier: Annotated[
        str, "Globally unique identifier for the team to retrieve memberships for."
    ],
    enable_pretty_output: Annotated[
        bool | None,
        "Boolean to enable pretty formatted output with line breaks and indentation. Recommended for debugging due to increased response size and time.",  # noqa: E501
    ] = None,
    optional_properties_to_include: Annotated[
        list[str] | None,
        "A list of optional properties to include in the results, given as an array of strings. Each string should be a property you wish to include.",  # noqa: E501
    ] = None,
    pagination_offset_token: Annotated[
        str | None,
        "Token to retrieve the next page of results. Use the offset provided in a previous response for pagination.",  # noqa: E501
    ] = None,
    results_per_page: Annotated[
        int | None, "Number of objects to return per page. Must be between 1 and 100."
    ] = None,
) -> Annotated[dict[str, Any], "Response from the API endpoint 'getTeamMembershipsForTeam'."]:
    """Retrieve the team memberships for a given team in Asana.

    Use this tool to obtain a list of team memberships for a specific team in Asana, providing insight into who is part of the team."""  # noqa: E501
    request_data: Any = None
    if isinstance(request_data, dict):
        request_data = remove_none_values(request_data)
    content = json.dumps(request_data) if request_data else None
    response = await make_request(
        url="https://app.asana.com/api/1.0/teams/{team_gid}/team_memberships".format(  # noqa: UP032
            team_gid=team_global_identifier
        ),
        method="GET",
        params=remove_none_values({
            "opt_pretty": enable_pretty_output,
            "limit": results_per_page,
            "offset": pagination_offset_token,
            "opt_fields": optional_properties_to_include,
        }),
        headers=remove_none_values({
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            )
        }),
        content=content,
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(requires_auth=OAuth2(id="arcade-asana", scopes=["team_memberships:read"]))
async def get_user_team_memberships(
    context: ToolContext,
    user_identifier: Annotated[
        str, "A string identifying a user, which can be 'me', an email, or the user's gid."
    ],
    workspace_identifier: Annotated[str, "Globally unique identifier for the workspace in Asana."],
    enable_pretty_output: Annotated[
        bool | None,
        "Enable pretty formatting for the response output with line breaks and indentation, useful for debugging. It increases response size and time.",  # noqa: E501
    ] = None,
    include_optional_fields: Annotated[
        list[str] | None,
        "A list of optional properties to include in the response, specified as strings. Use to get excluded fields.",  # noqa: E501
    ] = None,
    pagination_offset: Annotated[
        str | None,
        "Offset token for pagination. Use the offset to get the next page of results, returned from a previous paginated request.",  # noqa: E501
    ] = None,
    results_per_page: Annotated[
        int | None, "Number of records to return per page, ranging from 1 to 100."
    ] = None,
) -> Annotated[dict[str, Any], "Response from the API endpoint 'getTeamMembershipsForUser'."]:
    """Retrieve team membership records for a specific user.

    This tool returns the compact team membership records for a specified user in Asana. It should be called when you need to obtain information about a user's team memberships. Requires 'team_memberships:read' scope."""  # noqa: E501
    request_data: Any = None
    if isinstance(request_data, dict):
        request_data = remove_none_values(request_data)
    content = json.dumps(request_data) if request_data else None
    response = await make_request(
        url="https://app.asana.com/api/1.0/users/{user_gid}/team_memberships".format(  # noqa: UP032
            user_gid=user_identifier
        ),
        method="GET",
        params=remove_none_values({
            "opt_pretty": enable_pretty_output,
            "limit": results_per_page,
            "offset": pagination_offset,
            "workspace": workspace_identifier,
            "opt_fields": include_optional_fields,
        }),
        headers=remove_none_values({
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            )
        }),
        content=content,
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(requires_auth=OAuth2(id="arcade-asana"))
async def asana_create_team(
    context: ToolContext,
    mode: Annotated[
        ToolMode,
        "Operation mode: 'get_request_schema' returns the OpenAPI spec "
        "for the request body, 'execute' performs the actual operation",
    ],
    include_optional_properties: Annotated[
        list[str] | None,
        "Comma-separated list of optional properties to include in the response, enhancing the returned team's details.  Only used when mode is 'execute'.",  # noqa: E501
    ] = None,
    pretty_output_enabled: Annotated[
        bool | None,
        "Set to true to receive pretty, human-readable output with line breaks and indentation. Use for debugging.  Only used when mode is 'execute'.",  # noqa: E501
    ] = None,
    request_body: Annotated[
        str | None,
        "Stringified JSON representing the request body. Required when "
        "mode is 'execute', ignored when mode is 'get_request_schema'",
    ] = None,
) -> Annotated[dict[str, Any], "Response from the API endpoint 'createTeam'."]:
    """Create a team in your current Asana workspace.

    This tool is used to create a new team within the specified Asana workspace. It should be called when you need to organize a group of projects or tasks under a new team entity.

    Note: Understanding the request schema is necessary to properly create
    the stringified JSON input object for execution.\n\nThis operation also requires query parameters.

    Modes:
    - GET_REQUEST_SCHEMA: Returns the schema. Only call if you don't
      already have it. Do NOT call repeatedly if you already received
      the schema.
    - EXECUTE: Performs the operation with the provided request body
      JSON.\n      Note: You must also provide the required query parameters when executing.

    If you need the schema, call with mode='get_request_schema' ONCE, then execute.
    """  # noqa: E501
    if mode == ToolMode.GET_REQUEST_SCHEMA:
        return {
            "request_body_schema": REQUEST_BODY_SCHEMAS["ASANACREATETEAM"],
            "instructions": (
                "Use the request_body_schema to construct a valid JSON object. "
                "Once you have populated the object following the schema "
                "structure and requirements, call this tool again with "
                "mode='execute' and the stringified JSON as the "
                "request_body parameter along with the required query parameters. "
                "Do NOT call the schema mode again - you already have "
                "the schema now."
            ),
        }

    # Mode is EXECUTE - validate parameters

    # Validate request body is provided (not None or empty string)
    # Note: Empty objects like {} are allowed - schema validation will check if valid
    if request_body is None or request_body.strip() == "":
        raise RetryableToolError(
            message="Request body is required when mode is 'execute'",
            developer_message="The request_body parameter was null or empty string",
            additional_prompt_content=(
                "The request body is required to perform this operation. "
                "Use the schema below to construct a valid JSON object, "
                "then call this tool again in execute mode with the "
                "stringified JSON as the request_body parameter.\n\n"
                "Schema:\n\n```json\n" + REQUEST_BODY_SCHEMAS["ASANACREATETEAM"] + "\n```"
            ),
        )

    # Parse JSON
    try:
        request_data = json.loads(request_body)
    except json.JSONDecodeError as e:
        raise RetryableToolError(
            message=f"Invalid JSON in request body: {e!s}",
            developer_message=f"JSON parsing failed: {e!s}",
            additional_prompt_content=(
                f"The request body contains invalid JSON. Error: {e!s}\n\n"
                "Please provide a valid JSON string that matches the schema "
                "below, then call this tool again in execute mode.\n\n"
                "Schema:\n\n```json\n" + REQUEST_BODY_SCHEMAS["ASANACREATETEAM"] + "\n```"
            ),
        ) from e

    response = await make_request_with_schema_validation(
        url="https://app.asana.com/api/1.0/teams",
        method="POST",
        request_data=request_data,
        schema=REQUEST_BODY_SCHEMAS["ASANACREATETEAM"],
        params=remove_none_values({
            "opt_pretty": pretty_output_enabled,
            "opt_fields": include_optional_properties,
        }),
        headers=remove_none_values({
            "Content-Type": "application/json",
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            ),
        }),
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(requires_auth=OAuth2(id="arcade-asana", scopes=["teams:read"]))
async def get_asana_team_details(
    context: ToolContext,
    team_global_id: Annotated[
        str, "Globally unique identifier for the specific Asana team to retrieve details for."
    ],
    enable_pretty_output: Annotated[
        bool | None,
        "Set to true for human-readable formatting, with line breaks and indentation. Increases response time and size; use for debugging.",  # noqa: E501
    ] = None,
    include_optional_fields: Annotated[
        list[str] | None, "Comma-separated list of optional properties to include in the response."
    ] = None,
) -> Annotated[dict[str, Any], "Response from the API endpoint 'getTeam'."]:
    """Retrieve detailed information for a specific Asana team.

    Use this tool to obtain the complete details of a specific team in Asana. It requires the 'teams:read' scope and returns all available data for the team identified by its GID."""  # noqa: E501
    request_data: Any = None
    if isinstance(request_data, dict):
        request_data = remove_none_values(request_data)
    content = json.dumps(request_data) if request_data else None
    response = await make_request(
        url="https://app.asana.com/api/1.0/teams/{team_gid}".format(team_gid=team_global_id),  # noqa: UP032
        method="GET",
        params=remove_none_values({
            "opt_pretty": enable_pretty_output,
            "opt_fields": include_optional_fields,
        }),
        headers=remove_none_values({
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            )
        }),
        content=content,
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(requires_auth=OAuth2(id="arcade-asana"))
async def update_team_in_workspace(
    context: ToolContext,
    mode: Annotated[
        ToolMode,
        "Operation mode: 'get_request_schema' returns the OpenAPI spec "
        "for the request body, 'execute' performs the actual operation",
    ],
    team_global_id: Annotated[
        str | None,
        "Globally unique identifier for the team to update in the workspace. Required to specify which team will be modified.  Required when mode is 'execute', ignored when mode is 'get_request_schema'.",  # noqa: E501
    ] = None,
    include_optional_properties: Annotated[
        list[str] | None,
        "List of optional properties to include in the response. Use a list format for specifying multiple properties.  Only used when mode is 'execute'.",  # noqa: E501
    ] = None,
    enable_pretty_output: Annotated[
        bool | None,
        "Enable 'pretty' formatted output for easier readability in responses, increasing response size and time, mainly used for debugging.  Only used when mode is 'execute'.",  # noqa: E501
    ] = None,
    request_body: Annotated[
        str | None,
        "Stringified JSON representing the request body. Required when "
        "mode is 'execute', ignored when mode is 'get_request_schema'",
    ] = None,
) -> Annotated[dict[str, Any], "Response from the API endpoint 'updateTeam'."]:
    """Update a team within the current workspace.

    Use this tool to modify the details of a team in Asana's workspace. It should be called when you need to update team settings or information.

    Note: Understanding the request schema is necessary to properly create
    the stringified JSON input object for execution.\n\nThis operation also requires path, query parameters.

    Modes:
    - GET_REQUEST_SCHEMA: Returns the schema. Only call if you don't
      already have it. Do NOT call repeatedly if you already received
      the schema.
    - EXECUTE: Performs the operation with the provided request body
      JSON.\n      Note: You must also provide the required path, query parameters when executing.

    If you need the schema, call with mode='get_request_schema' ONCE, then execute.
    """  # noqa: E501
    if mode == ToolMode.GET_REQUEST_SCHEMA:
        return {
            "request_body_schema": REQUEST_BODY_SCHEMAS["UPDATETEAMINWORKSPACE"],
            "instructions": (
                "Use the request_body_schema to construct a valid JSON object. "
                "Once you have populated the object following the schema "
                "structure and requirements, call this tool again with "
                "mode='execute' and the stringified JSON as the "
                "request_body parameter along with the required path, query parameters. "
                "Do NOT call the schema mode again - you already have "
                "the schema now."
            ),
        }

    # Mode is EXECUTE - validate parameters
    # Validate required parameters
    missing_params = []
    if not team_global_id:
        missing_params.append(("team_global_id", "path"))

    if missing_params:
        param_names = [p[0] for p in missing_params]
        param_details = ", ".join([f"{p[0]} ({p[1]})" for p in missing_params])
        raise RetryableToolError(
            message=f"Missing required parameters: {param_names}",
            developer_message=(f"Required parameters validation failed: {param_details}"),
            additional_prompt_content=(
                f"The following required parameters are missing: "
                f"{param_details}. Please call this tool again with all "
                "required parameters."
            ),
        )

    # Validate request body is provided (not None or empty string)
    # Note: Empty objects like {} are allowed - schema validation will check if valid
    if request_body is None or request_body.strip() == "":
        raise RetryableToolError(
            message="Request body is required when mode is 'execute'",
            developer_message="The request_body parameter was null or empty string",
            additional_prompt_content=(
                "The request body is required to perform this operation. "
                "Use the schema below to construct a valid JSON object, "
                "then call this tool again in execute mode with the "
                "stringified JSON as the request_body parameter.\n\n"
                "Schema:\n\n```json\n" + REQUEST_BODY_SCHEMAS["UPDATETEAMINWORKSPACE"] + "\n```"
            ),
        )

    # Parse JSON
    try:
        request_data = json.loads(request_body)
    except json.JSONDecodeError as e:
        raise RetryableToolError(
            message=f"Invalid JSON in request body: {e!s}",
            developer_message=f"JSON parsing failed: {e!s}",
            additional_prompt_content=(
                f"The request body contains invalid JSON. Error: {e!s}\n\n"
                "Please provide a valid JSON string that matches the schema "
                "below, then call this tool again in execute mode.\n\n"
                "Schema:\n\n```json\n" + REQUEST_BODY_SCHEMAS["UPDATETEAMINWORKSPACE"] + "\n```"
            ),
        ) from e

    response = await make_request_with_schema_validation(
        url="https://app.asana.com/api/1.0/teams/{team_gid}".format(team_gid=team_global_id),  # noqa: UP032
        method="PUT",
        request_data=request_data,
        schema=REQUEST_BODY_SCHEMAS["UPDATETEAMINWORKSPACE"],
        params=remove_none_values({
            "opt_pretty": enable_pretty_output,
            "opt_fields": include_optional_properties,
        }),
        headers=remove_none_values({
            "Content-Type": "application/json",
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            ),
        }),
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(requires_auth=OAuth2(id="arcade-asana", scopes=["teams:read"]))
async def get_asana_teams_for_workspace(
    context: ToolContext,
    workspace_global_id: Annotated[
        str, "Globally unique identifier for the workspace or organization in Asana."
    ],
    enable_pretty_output: Annotated[
        bool | None,
        "Set to true to receive the response in a 'pretty' JSON format with line breaks and indentation. Useful for debugging but increases response time and size.",  # noqa: E501
    ] = None,
    include_optional_fields: Annotated[
        list[str] | None, "Comma-separated list of optional properties to include in the response."
    ] = None,
    pagination_offset_token: Annotated[
        str | None,
        "An offset token for pagination. Use a token from a previous request to retrieve the next page of results. If not provided, the first page is returned.",  # noqa: E501
    ] = None,
    results_per_page: Annotated[
        int | None, "Number of teams to return per page, between 1 and 100."
    ] = None,
) -> Annotated[dict[str, Any], "Response from the API endpoint 'getTeamsForWorkspace'."]:
    """Retrieve all teams for a specified Asana workspace.

    Use this tool to get a list of all teams in an Asana workspace visible to the user. Ensure the required scope 'teams:read' is granted."""  # noqa: E501
    request_data: Any = None
    if isinstance(request_data, dict):
        request_data = remove_none_values(request_data)
    content = json.dumps(request_data) if request_data else None
    response = await make_request(
        url="https://app.asana.com/api/1.0/workspaces/{workspace_gid}/teams".format(  # noqa: UP032
            workspace_gid=workspace_global_id
        ),
        method="GET",
        params=remove_none_values({
            "opt_pretty": enable_pretty_output,
            "limit": results_per_page,
            "offset": pagination_offset_token,
            "opt_fields": include_optional_fields,
        }),
        headers=remove_none_values({
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            )
        }),
        content=content,
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(requires_auth=OAuth2(id="arcade-asana", scopes=["teams:read"]))
async def get_asana_teams_for_user(
    context: ToolContext,
    filter_by_organization: Annotated[
        str, "Specify the workspace or organization to filter the teams on for the Asana user."
    ],
    user_identifier: Annotated[
        str, "A string identifying a user. Accepts 'me', an email, or user's gid."
    ],
    enable_pretty_output: Annotated[
        bool | None,
        "Set to true to format the response with line breaks and indentation. Useful for debugging but can increase response size.",  # noqa: E501
    ] = None,
    include_optional_fields: Annotated[
        list[str] | None, "Comma-separated list of optional properties to include in the response."
    ] = None,
    pagination_offset_token: Annotated[
        str | None,
        "Offset token to navigate through paginated results. Use an offset from a previous request to get the next page. If not provided, the first page is returned.",  # noqa: E501
    ] = None,
    results_per_page: Annotated[
        int | None, "Number of team records to return per page. Must be between 1 and 100."
    ] = None,
) -> Annotated[dict[str, Any], "Response from the API endpoint 'getTeamsForUser'."]:
    """Get all teams assigned to a specific Asana user.

    Use this tool to retrieve a list of all teams that a specific user is part of in Asana. Requires the 'teams:read' permission scope."""  # noqa: E501
    request_data: Any = None
    if isinstance(request_data, dict):
        request_data = remove_none_values(request_data)
    content = json.dumps(request_data) if request_data else None
    response = await make_request(
        url="https://app.asana.com/api/1.0/users/{user_gid}/teams".format(user_gid=user_identifier),  # noqa: UP032
        method="GET",
        params=remove_none_values({
            "opt_pretty": enable_pretty_output,
            "limit": results_per_page,
            "offset": pagination_offset_token,
            "organization": filter_by_organization,
            "opt_fields": include_optional_fields,
        }),
        headers=remove_none_values({
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            )
        }),
        content=content,
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(requires_auth=OAuth2(id="arcade-asana"))
async def add_user_to_team(
    context: ToolContext,
    team_unique_identifier: Annotated[
        str, "Globally unique identifier for the team to which the user is being added."
    ],
    enable_pretty_output: Annotated[
        bool | None,
        "Set to true for a readable response format with line breaks and indentation. Use mainly for debugging due to increased response time and size.",  # noqa: E501
    ] = None,
    include_optional_properties: Annotated[
        list[str] | None,
        "Comma-separated list of properties to include in the response, which are excluded by default.",  # noqa: E501
    ] = None,
    user_identifier: Annotated[
        str | None,
        'Identifies the user to add. Use "me", an email, or the user\'s global ID (gid).',
    ] = None,
) -> Annotated[dict[str, Any], "Response from the API endpoint 'addUserForTeam'."]:
    """Adds a user to a specified team on Asana.

    This tool should be called when you need to add an existing user to an Asana team. The caller must be a member of the team and the user being added must belong to the same organization as the team."""  # noqa: E501
    request_data: Any = {"data": {"user": user_identifier}}
    if isinstance(request_data, dict):
        request_data = remove_none_values(request_data)
    content = json.dumps(request_data) if request_data else None
    response = await make_request(
        url="https://app.asana.com/api/1.0/teams/{team_gid}/addUser".format(  # noqa: UP032
            team_gid=team_unique_identifier
        ),
        method="POST",
        params=remove_none_values({
            "opt_pretty": enable_pretty_output,
            "opt_fields": include_optional_properties,
        }),
        headers=remove_none_values({
            "Content-Type": "application/json",
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            ),
        }),
        content=content,
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(requires_auth=OAuth2(id="arcade-asana"))
async def remove_user_from_team(
    context: ToolContext,
    team_global_identifier: Annotated[
        str, "Globally unique identifier for the team to remove a user from."
    ],
    enable_pretty_output: Annotated[
        bool | None,
        "Enable pretty JSON formatting for the response. This increases readability but also response size, so it's advisable for debugging.",  # noqa: E501
    ] = None,
    user_identifier: Annotated[
        str | None,
        "A string identifying the user to be removed. It can be 'me', an email, or the user's gid.",
    ] = None,
) -> Annotated[dict[str, Any], "Response from the API endpoint 'removeUserForTeam'."]:
    """Removes a user from a specified Asana team.

    Use this tool to remove a user from an Asana team. The request must be made by someone who is a member of the team."""  # noqa: E501
    request_data: Any = {"data": {"user": user_identifier}}
    if isinstance(request_data, dict):
        request_data = remove_none_values(request_data)
    content = json.dumps(request_data) if request_data else None
    response = await make_request(
        url="https://app.asana.com/api/1.0/teams/{team_gid}/removeUser".format(  # noqa: UP032
            team_gid=team_global_identifier
        ),
        method="POST",
        params=remove_none_values({"opt_pretty": enable_pretty_output}),
        headers=remove_none_values({
            "Content-Type": "application/json",
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            ),
        }),
        content=content,
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(requires_auth=OAuth2(id="arcade-asana"))
async def get_time_period_record(
    context: ToolContext,
    time_period_id: Annotated[
        str, "Globally unique identifier for the time period to retrieve its record."
    ],
    enable_pretty_output: Annotated[
        bool | None,
        "Enable pretty formatting for output. Use only for debugging as it increases response size and processing time.",  # noqa: E501
    ] = None,
    include_optional_properties: Annotated[
        list[str] | None,
        "List of property names to include in the response. These properties are excluded by default.",  # noqa: E501
    ] = None,
) -> Annotated[dict[str, Any], "Response from the API endpoint 'getTimePeriod'."]:
    """Retrieve detailed information for a specific time period.

    Use this tool to get the full record of a single time period by specifying its identifier."""
    request_data: Any = None
    if isinstance(request_data, dict):
        request_data = remove_none_values(request_data)
    content = json.dumps(request_data) if request_data else None
    response = await make_request(
        url="https://app.asana.com/api/1.0/time_periods/{time_period_gid}".format(  # noqa: UP032
            time_period_gid=time_period_id
        ),
        method="GET",
        params=remove_none_values({
            "opt_pretty": enable_pretty_output,
            "opt_fields": include_optional_properties,
        }),
        headers=remove_none_values({
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            )
        }),
        content=content,
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(requires_auth=OAuth2(id="arcade-asana"))
async def get_time_periods(
    context: ToolContext,
    workspace_id: Annotated[str, "Globally unique identifier for the Asana workspace."],
    enable_pretty_output: Annotated[
        bool | None,
        "Set to true to receive formatted, readable output mainly for debugging, increasing response size.",  # noqa: E501
    ] = None,
    end_date: Annotated[
        str | None,
        "The end date for time periods in ISO 8601 format. Specify the last date to include in the returned results.",  # noqa: E501
    ] = None,
    include_optional_properties: Annotated[
        list[str] | None,
        "A list of optional property names to include in the response, specified as an array of strings.",  # noqa: E501
    ] = None,
    pagination_offset: Annotated[
        str | None,
        "Offset token for pagination. Use the offset from a previous API response to retrieve the next page of results. If omitted, the first page is returned.",  # noqa: E501
    ] = None,
    results_per_page: Annotated[
        int | None, "The number of results to return per page, from 1 to 100."
    ] = None,
    start_date: Annotated[
        str | None,
        "Start date for the time period in ISO 8601 format. Determines the beginning of the time period records to be retrieved.",  # noqa: E501
    ] = None,
) -> Annotated[dict[str, Any], "Response from the API endpoint 'getTimePeriods'."]:
    """Retrieve compact time period records from Asana.

    Use this tool to access and retrieve compact time period records from Asana's service when you need to handle or analyze time-related data."""  # noqa: E501
    request_data: Any = None
    if isinstance(request_data, dict):
        request_data = remove_none_values(request_data)
    content = json.dumps(request_data) if request_data else None
    response = await make_request(
        url="https://app.asana.com/api/1.0/time_periods",
        method="GET",
        params=remove_none_values({
            "opt_pretty": enable_pretty_output,
            "limit": results_per_page,
            "offset": pagination_offset,
            "start_on": start_date,
            "end_on": end_date,
            "workspace": workspace_id,
            "opt_fields": include_optional_properties,
        }),
        headers=remove_none_values({
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            )
        }),
        content=content,
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(requires_auth=OAuth2(id="arcade-asana", scopes=["time_tracking_entries:read"]))
async def get_time_tracking_entries(
    context: ToolContext,
    task_id: Annotated[
        str, "The unique identifier for the task to retrieve time tracking entries for."
    ],
    enable_pretty_output: Annotated[
        bool | None,
        "Set to true for a readable, pretty-formatted output. Recommended for debugging as it may increase response time and size.",  # noqa: E501
    ] = None,
    include_optional_fields: Annotated[
        list[str] | None,
        "Comma-separated list of optional properties to include in the response, enhancing the returned resource details.",  # noqa: E501
    ] = None,
    pagination_offset: Annotated[
        str | None,
        "Token for pagination; use to retrieve the next set of results. If not provided, the first page is returned.",  # noqa: E501
    ] = None,
    results_per_page: Annotated[
        int | None, "Number of time tracking entries to return per page, must be between 1 and 100."
    ] = None,
) -> Annotated[dict[str, Any], "Response from the API endpoint 'getTimeTrackingEntriesForTask'."]:
    """Retrieve time tracking entries for a specified task.

    Use this tool to get detailed time tracking entries associated with a specific task in Asana. It should be called when you need to analyze or review the time spent on a task by different users."""  # noqa: E501
    request_data: Any = None
    if isinstance(request_data, dict):
        request_data = remove_none_values(request_data)
    content = json.dumps(request_data) if request_data else None
    response = await make_request(
        url="https://app.asana.com/api/1.0/tasks/{task_gid}/time_tracking_entries".format(  # noqa: UP032
            task_gid=task_id
        ),
        method="GET",
        params=remove_none_values({
            "opt_pretty": enable_pretty_output,
            "limit": results_per_page,
            "offset": pagination_offset,
            "opt_fields": include_optional_fields,
        }),
        headers=remove_none_values({
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            )
        }),
        content=content,
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(requires_auth=OAuth2(id="arcade-asana"))
async def create_time_tracking_entry(
    context: ToolContext,
    task_id: Annotated[str, "The unique identifier for the task to operate on."],
    duration_minutes_tracked: Annotated[
        int | None, "Time in minutes tracked by the entry. Must be greater than 0."
    ] = None,
    enable_pretty_output: Annotated[
        bool | None,
        "Set to true to receive formatted, human-readable JSON responses. Useful for debugging as it may slow down the response.",  # noqa: E501
    ] = None,
    entry_logged_date: Annotated[
        str | None,
        "Optional. The date the time entry is logged. Defaults to today if not specified.",
    ] = None,
    include_optional_properties: Annotated[
        list[str] | None,
        "A list of optional property names to include in the response. This should be used to specify which additional fields you want to include in the return data format when creating a time tracking entry.",  # noqa: E501
    ] = None,
    project_gid_attribution: Annotated[
        str | None,
        "Optional. The GID of the project to which the tracked time is attributed. If not provided, no project attribution is made.",  # noqa: E501
    ] = None,
) -> Annotated[dict[str, Any], "Response from the API endpoint 'createTimeTrackingEntry'."]:
    """Create a time tracking entry on a task.

    Use this tool to log time against a specific task in Asana. It returns the information of the newly created time tracking entry, helping to keep task hours updated."""  # noqa: E501
    request_data: Any = {
        "data": {
            "duration_minutes": duration_minutes_tracked,
            "entered_on": entry_logged_date,
            "attributable_to": project_gid_attribution,
        }
    }
    if isinstance(request_data, dict):
        request_data = remove_none_values(request_data)
    content = json.dumps(request_data) if request_data else None
    response = await make_request(
        url="https://app.asana.com/api/1.0/tasks/{task_gid}/time_tracking_entries".format(  # noqa: UP032
            task_gid=task_id
        ),
        method="POST",
        params=remove_none_values({
            "opt_pretty": enable_pretty_output,
            "opt_fields": include_optional_properties,
        }),
        headers=remove_none_values({
            "Content-Type": "application/json",
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            ),
        }),
        content=content,
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(requires_auth=OAuth2(id="arcade-asana", scopes=["time_tracking_entries:read"]))
async def get_time_tracking_entry(
    context: ToolContext,
    time_tracking_entry_id: Annotated[
        str,
        "Globally unique identifier for the time tracking entry. Used to specify which entry to retrieve.",  # noqa: E501
    ],
    enable_pretty_output: Annotated[
        bool | None,
        "Enable to get the response in a readable format with line breaks and indentation. Use only for debugging, as it may increase response time and size.",  # noqa: E501
    ] = None,
    include_optional_properties: Annotated[
        list[str] | None,
        "A list of property names to include as optional fields in the response. Provide properties as strings in a comma-separated format. Default properties are excluded unless explicitly specified here.",  # noqa: E501
    ] = None,
) -> Annotated[dict[str, Any], "Response from the API endpoint 'getTimeTrackingEntry'."]:
    """Retrieve a time tracking entry from Asana.

    Use this tool to obtain the complete record of a specific time tracking entry in Asana. This should be called when detailed information about a particular time tracking task is needed."""  # noqa: E501
    request_data: Any = None
    if isinstance(request_data, dict):
        request_data = remove_none_values(request_data)
    content = json.dumps(request_data) if request_data else None
    response = await make_request(
        url="https://app.asana.com/api/1.0/time_tracking_entries/{time_tracking_entry_gid}".format(  # noqa: UP032
            time_tracking_entry_gid=time_tracking_entry_id
        ),
        method="GET",
        params=remove_none_values({
            "opt_pretty": enable_pretty_output,
            "opt_fields": include_optional_properties,
        }),
        headers=remove_none_values({
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            )
        }),
        content=content,
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(requires_auth=OAuth2(id="arcade-asana"))
async def update_time_tracking_entry(
    context: ToolContext,
    time_tracking_entry_id: Annotated[
        str, "Globally unique identifier for the time tracking entry to be updated."
    ],
    duration_minutes_tracked: Annotated[
        int | None, "The time in minutes tracked by the entry. Optional field."
    ] = None,
    enable_pretty_output: Annotated[
        bool | None,
        "Enable formatted output for easier readability. Use mainly for debugging as it increases response time and size.",  # noqa: E501
    ] = None,
    entry_logged_date: Annotated[
        str | None,
        "The date the entry is logged. Defaults to today if not specified. Use format 'YYYY-MM-DD'.",  # noqa: E501
    ] = None,
    include_optional_properties: Annotated[
        list[str] | None, "Comma-separated list of optional properties to include in the response."
    ] = None,
    project_attributable_gid: Annotated[
        str | None,
        "Optional. The globally unique identifier (gid) of the project to which the time is attributable.",  # noqa: E501
    ] = None,
) -> Annotated[dict[str, Any], "Response from the API endpoint 'updateTimeTrackingEntry'."]:
    """Updates an existing time tracking entry in Asana.

    Use this tool to update specific fields of an existing time tracking entry in Asana by specifying only the fields you wish to change. It helps avoid overwriting changes made by others and returns the updated record."""  # noqa: E501
    request_data: Any = {
        "data": {
            "duration_minutes": duration_minutes_tracked,
            "entered_on": entry_logged_date,
            "attributable_to": project_attributable_gid,
        }
    }
    if isinstance(request_data, dict):
        request_data = remove_none_values(request_data)
    content = json.dumps(request_data) if request_data else None
    response = await make_request(
        url="https://app.asana.com/api/1.0/time_tracking_entries/{time_tracking_entry_gid}".format(  # noqa: UP032
            time_tracking_entry_gid=time_tracking_entry_id
        ),
        method="PUT",
        params=remove_none_values({
            "opt_pretty": enable_pretty_output,
            "opt_fields": include_optional_properties,
        }),
        headers=remove_none_values({
            "Content-Type": "application/json",
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            ),
        }),
        content=content,
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(requires_auth=OAuth2(id="arcade-asana"))
async def delete_time_tracking_entry(
    context: ToolContext,
    time_tracking_entry_identifier: Annotated[
        str, "Globally unique identifier for the time tracking entry to be deleted."
    ],
    enable_pretty_output: Annotated[
        bool | None,
        "Set to true for a formatted, readable JSON response. Use for debugging; increases response time and size.",  # noqa: E501
    ] = None,
) -> Annotated[dict[str, Any], "Response from the API endpoint 'deleteTimeTrackingEntry'."]:
    """Delete a specific time tracking entry in Asana.

    Use this tool to delete an existing time tracking entry by specifying its unique identifier in Asana. Useful for managing and correcting time tracking data."""  # noqa: E501
    request_data: Any = None
    if isinstance(request_data, dict):
        request_data = remove_none_values(request_data)
    content = json.dumps(request_data) if request_data else None
    response = await make_request(
        url="https://app.asana.com/api/1.0/time_tracking_entries/{time_tracking_entry_gid}".format(  # noqa: UP032
            time_tracking_entry_gid=time_tracking_entry_identifier
        ),
        method="DELETE",
        params=remove_none_values({"opt_pretty": enable_pretty_output}),
        headers=remove_none_values({
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            )
        }),
        content=content,
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(requires_auth=OAuth2(id="arcade-asana", scopes=["time_tracking_entries:read"]))
async def fetch_time_tracking_data(
    context: ToolContext,
    enable_pretty_output: Annotated[
        bool | None,
        "Enable pretty formatting for the response. Useful for debugging, but increases response time and size.",  # noqa: E501
    ] = None,
    include_optional_fields: Annotated[
        list[str] | None,
        "Include optional properties in the response by listing field names as a comma-separated array. This enhances the response data detail.",  # noqa: E501
    ] = None,
    pagination_offset_token: Annotated[
        str | None,
        "Token to specify the starting point for retrieving the next page of results. Use the offset returned from a previous request. If not provided, the first page is returned.",  # noqa: E501
    ] = None,
    portfolio_id: Annotated[
        str | None,
        "Globally unique identifier for the portfolio to filter time tracking entries by.",
    ] = None,
    project_identifier_attribution: Annotated[
        str | None, "Unique ID for the project to filter time tracking entries by attribution."
    ] = None,
    results_per_page: Annotated[
        int | None,
        "Specify the number of time tracking entries to return per page. Acceptable values are between 1 and 100.",  # noqa: E501
    ] = None,
    task_id: Annotated[
        str | None,
        "Globally unique identifier for the task to filter time tracking entries by. This is used to specify the task whose time tracking entries you want to retrieve.",  # noqa: E501
    ] = None,
    user_id_filter: Annotated[
        str | None, "Globally unique identifier for the user to filter time tracking entries by."
    ] = None,
    workspace_identifier: Annotated[
        str | None,
        "Globally unique identifier for the Asana workspace to filter the time tracking entries.",
    ] = None,
) -> Annotated[dict[str, Any], "Response from the API endpoint 'getTimeTrackingEntries'."]:
    """Fetch time tracking entries from Asana.

    Retrieve a list of time tracking entries filtered by specific tasks, attributed projects, portfolios, or users in Asana. Requires the 'time_tracking_entries:read' scope."""  # noqa: E501
    request_data: Any = None
    if isinstance(request_data, dict):
        request_data = remove_none_values(request_data)
    content = json.dumps(request_data) if request_data else None
    response = await make_request(
        url="https://app.asana.com/api/1.0/time_tracking_entries",
        method="GET",
        params=remove_none_values({
            "opt_pretty": enable_pretty_output,
            "task": task_id,
            "attributable_to": project_identifier_attribution,
            "portfolio": portfolio_id,
            "user": user_id_filter,
            "workspace": workspace_identifier,
            "limit": results_per_page,
            "offset": pagination_offset_token,
            "opt_fields": include_optional_fields,
        }),
        headers=remove_none_values({
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            )
        }),
        content=content,
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(requires_auth=OAuth2(id="arcade-asana", scopes=["workspace.typeahead:read"]))
async def workspace_typeahead_search(
    context: ToolContext,
    workspace_id: Annotated[str, "Globally unique identifier for the workspace or organization."],
    enable_pretty_output: Annotated[
        bool | None,
        "Enable pretty format for the response with proper indentation. Use for debugging due to increased response size, but not recommended for production.",  # noqa: E501
    ] = None,
    include_optional_fields: Annotated[
        list[str] | None,
        "A list of properties to include in the response, set as a comma-separated list to include optional fields in the result.",  # noqa: E501
    ] = None,
    resource_type: Annotated[
        str | None,
        "Specify the type of workspace objects to return, such as 'custom_field', 'portfolio', 'project', 'tag', 'task', or 'user'.",  # noqa: E501
    ] = "user",
    results_count: Annotated[
        int | None, "The number of results to return, ranging from 1 to 100. Default is 20."
    ] = None,
    search_query: Annotated[
        str | None,
        "String to search for relevant workspace objects. An empty string will return results.",
    ] = None,
    search_result_resource_type: Annotated[
        str,
        "Specify the type of objects to return in the typeahead search. Options: custom_field, goal, project, project_template, portfolio, tag, task, team, user. Only one type can be used at a time.",  # noqa: E501
    ] = "user",
) -> Annotated[dict[str, Any], "Response from the API endpoint 'typeaheadForWorkspace'."]:
    """Retrieve workspace objects using a typeahead search.

    This tool fetches objects within a workspace via a typeahead search, providing quick, but not exhaustive or highly accurate results. It's useful for implementing auto-completion features by obtaining a compact representation of workspace objects, such as user, project, task, or project_template, ordered by relevance criteria specific to each type."""  # noqa: E501
    request_data: Any = None
    if isinstance(request_data, dict):
        request_data = remove_none_values(request_data)
    content = json.dumps(request_data) if request_data else None
    response = await make_request(
        url="https://app.asana.com/api/1.0/workspaces/{workspace_gid}/typeahead".format(  # noqa: UP032
            workspace_gid=workspace_id
        ),
        method="GET",
        params=remove_none_values({
            "resource_type": search_result_resource_type,
            "type": resource_type,
            "query": search_query,
            "count": results_count,
            "opt_pretty": enable_pretty_output,
            "opt_fields": include_optional_fields,
        }),
        headers=remove_none_values({
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            )
        }),
        content=content,
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(requires_auth=OAuth2(id="arcade-asana", scopes=["tasks:read"]))
async def fetch_user_task_details(
    context: ToolContext,
    user_task_list_global_id: Annotated[
        str, "Globally unique identifier for the user task list in Asana."
    ],
    enable_pretty_output: Annotated[
        bool | None,
        "Set to true for readable, indented JSON output. Use primarily for debugging due to increased response size.",  # noqa: E501
    ] = None,
    include_optional_properties: Annotated[
        list[str] | None,
        "Specify which optional properties should be included in the response. Provide as a list of strings.",  # noqa: E501
    ] = None,
) -> Annotated[dict[str, Any], "Response from the API endpoint 'getUserTaskList'."]:
    """Retrieve the full record for a user task list.

    Call this tool to obtain detailed information about a specific user's task list in Asana. This requires read permissions on tasks."""  # noqa: E501
    request_data: Any = None
    if isinstance(request_data, dict):
        request_data = remove_none_values(request_data)
    content = json.dumps(request_data) if request_data else None
    response = await make_request(
        url="https://app.asana.com/api/1.0/user_task_lists/{user_task_list_gid}".format(  # noqa: UP032
            user_task_list_gid=user_task_list_global_id
        ),
        method="GET",
        params=remove_none_values({
            "opt_pretty": enable_pretty_output,
            "opt_fields": include_optional_properties,
        }),
        headers=remove_none_values({
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            )
        }),
        content=content,
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(requires_auth=OAuth2(id="arcade-asana", scopes=["tasks:read"]))
async def fetch_user_tasks(
    context: ToolContext,
    user_identifier: Annotated[
        str, 'A string to identify the user, either "me", an email, or the user\'s gid.'
    ],
    workspace_id: Annotated[
        str, "The ID of the workspace to retrieve the user's task list from in Asana."
    ],
    enable_pretty_output: Annotated[
        bool | None,
        "Enable pretty formatting for JSON responses. When true, the response includes line breaks and indentation for better readability. This increases response size and processing time, so use primarily for debugging.",  # noqa: E501
    ] = None,
    include_optional_properties: Annotated[
        list[str] | None,
        "A list of optional properties to include in the response, specified as an array of strings.",  # noqa: E501
    ] = None,
) -> Annotated[dict[str, Any], "Response from the API endpoint 'getUserTaskListForUser'."]:
    """Fetch the full task list record for a user from Asana.

    This tool retrieves the complete task list for a specified user in Asana, requiring the 'tasks:read' permission."""  # noqa: E501
    request_data: Any = None
    if isinstance(request_data, dict):
        request_data = remove_none_values(request_data)
    content = json.dumps(request_data) if request_data else None
    response = await make_request(
        url="https://app.asana.com/api/1.0/users/{user_gid}/user_task_list".format(  # noqa: UP032
            user_gid=user_identifier
        ),
        method="GET",
        params=remove_none_values({
            "opt_pretty": enable_pretty_output,
            "workspace": workspace_id,
            "opt_fields": include_optional_properties,
        }),
        headers=remove_none_values({
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            )
        }),
        content=content,
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(requires_auth=OAuth2(id="arcade-asana", scopes=["users:read"]))
async def get_asana_users(
    context: ToolContext,
    enable_pretty_output: Annotated[
        bool | None,
        "Enable pretty formatting of the response for easier readability. Recommended for debugging as it increases response size and time.",  # noqa: E501
    ] = None,
    filter_by_team_id: Annotated[
        str | None,
        "The team ID to filter users in Asana. It allows you to specify a particular team to narrow down the user results.",  # noqa: E501
    ] = None,
    include_optional_properties: Annotated[
        list[str] | None,
        "Comma-separated list of optional user properties to include in the response. Use this to fetch additional fields excluded by default.",  # noqa: E501
    ] = None,
    pagination_offset_token: Annotated[
        str | None,
        "Offset token for pagination, used to fetch subsequent pages of results. Only valid when using an offset returned in a prior request.",  # noqa: E501
    ] = None,
    results_per_page: Annotated[
        int | None, "The number of user records to return per page. Must be between 1 and 100."
    ] = None,
    workspace_id: Annotated[
        str | None, "The ID of the workspace or organization to filter users on."
    ] = None,
) -> Annotated[dict[str, Any], "Response from the API endpoint 'getUsers'."]:
    """Retrieve Asana user records across workspaces.

    Fetches user records from Asana for all users in workspaces and organizations accessible to the authenticated user. An optional workspace ID can be provided to filter results. The data is sorted by user ID and requires the 'users:read' scope."""  # noqa: E501
    request_data: Any = None
    if isinstance(request_data, dict):
        request_data = remove_none_values(request_data)
    content = json.dumps(request_data) if request_data else None
    response = await make_request(
        url="https://app.asana.com/api/1.0/users",
        method="GET",
        params=remove_none_values({
            "workspace": workspace_id,
            "team": filter_by_team_id,
            "opt_pretty": enable_pretty_output,
            "limit": results_per_page,
            "offset": pagination_offset_token,
            "opt_fields": include_optional_properties,
        }),
        headers=remove_none_values({
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            )
        }),
        content=content,
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(requires_auth=OAuth2(id="arcade-asana", scopes=["users:read"]))
async def get_user_details(
    context: ToolContext,
    user_identifier: Annotated[
        str, "A string to identify a user, which can be 'me', an email, or a user's gid."
    ],
    enable_pretty_output: Annotated[
        bool | None,
        "Set to true to format the response for readability with line breaks and indentation. Recommended for debugging, as it may increase response time and size.",  # noqa: E501
    ] = None,
    include_optional_properties: Annotated[
        list[str] | None,
        "Specify properties to include in the response using a comma-separated list of strings.",
    ] = None,
) -> Annotated[dict[str, Any], "Response from the API endpoint 'getUser'."]:
    """Retrieve detailed user information from Asana.

    Use this tool to get the complete user record for a specific user in Asana by providing their user ID. Ensure you have the required scope: users:read."""  # noqa: E501
    request_data: Any = None
    if isinstance(request_data, dict):
        request_data = remove_none_values(request_data)
    content = json.dumps(request_data) if request_data else None
    response = await make_request(
        url="https://app.asana.com/api/1.0/users/{user_gid}".format(user_gid=user_identifier),  # noqa: UP032
        method="GET",
        params=remove_none_values({
            "opt_pretty": enable_pretty_output,
            "opt_fields": include_optional_properties,
        }),
        headers=remove_none_values({
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            )
        }),
        content=content,
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(requires_auth=OAuth2(id="arcade-asana", scopes=["users:read"]))
async def get_user_favorites(
    context: ToolContext,
    user_identifier: Annotated[str, "String to identify a user: 'me', an email, or a user GID."],
    workspace_id: Annotated[
        str, "The unique identifier of the Asana workspace to retrieve favorites from."
    ],
    favorites_resource_type: Annotated[
        str, "Specifies the type of favorites to return (e.g., 'portfolio', 'project')."
    ] = "project",
    include_optional_fields: Annotated[
        list[str] | None,
        "Specify a list of optional properties to include in the response, separated by commas.",
    ] = None,
    pagination_offset: Annotated[
        str | None,
        "An offset token for pagination. Use a token returned from a previous request to navigate pages. If not provided, the first page is returned.",  # noqa: E501
    ] = None,
    pretty_output: Annotated[
        bool | None,
        "Set to true for readable, formatted output with line breaks and indentation, ideal for debugging.",  # noqa: E501
    ] = None,
    results_per_page: Annotated[
        int | None, "Specifies the number of objects to return per page, between 1 and 100."
    ] = None,
) -> Annotated[dict[str, Any], "Response from the API endpoint 'getFavoritesForUser'."]:
    """Retrieve a user's favorites from a specified Asana workspace.

    This tool retrieves all of a user's favorites within a specified Asana workspace and of a given type. The results are ordered as they appear in the user's Asana sidebar and are limited to the current user associated with the authentication token."""  # noqa: E501
    request_data: Any = None
    if isinstance(request_data, dict):
        request_data = remove_none_values(request_data)
    content = json.dumps(request_data) if request_data else None
    response = await make_request(
        url="https://app.asana.com/api/1.0/users/{user_gid}/favorites".format(  # noqa: UP032
            user_gid=user_identifier
        ),
        method="GET",
        params=remove_none_values({
            "opt_pretty": pretty_output,
            "limit": results_per_page,
            "offset": pagination_offset,
            "resource_type": favorites_resource_type,
            "workspace": workspace_id,
            "opt_fields": include_optional_fields,
        }),
        headers=remove_none_values({
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            )
        }),
        content=content,
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(requires_auth=OAuth2(id="arcade-asana", scopes=["users:read"]))
async def get_team_users_asana(
    context: ToolContext,
    team_global_identifier: Annotated[
        str, "Globally unique identifier for the Asana team to retrieve users from."
    ],
    enable_pretty_output: Annotated[
        bool | None,
        "Enable pretty output with line breaks and indentation for readability. Recommended for debugging as it increases response time and size.",  # noqa: E501
    ] = None,
    include_optional_fields: Annotated[
        list[str] | None,
        "Comma-separated list of properties you wish to include in the response. These are excluded by default.",  # noqa: E501
    ] = None,
    pagination_offset_token: Annotated[
        str | None,
        "Offset token for pagination. Use this token from a previous response to fetch the next page of results.",  # noqa: E501
    ] = None,
) -> Annotated[dict[str, Any], "Response from the API endpoint 'getUsersForTeam'."]:
    """Retrieve user records for a specific Asana team.

    This tool fetches the compact records of all users who are members of a specified Asana team. It should be called to obtain a list of team members, sorted alphabetically, with a limit of 2000 users."""  # noqa: E501
    request_data: Any = None
    if isinstance(request_data, dict):
        request_data = remove_none_values(request_data)
    content = json.dumps(request_data) if request_data else None
    response = await make_request(
        url="https://app.asana.com/api/1.0/teams/{team_gid}/users".format(  # noqa: UP032
            team_gid=team_global_identifier
        ),
        method="GET",
        params=remove_none_values({
            "opt_pretty": enable_pretty_output,
            "offset": pagination_offset_token,
            "opt_fields": include_optional_fields,
        }),
        headers=remove_none_values({
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            )
        }),
        content=content,
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(requires_auth=OAuth2(id="arcade-asana", scopes=["users:read"]))
async def get_workspace_users(
    context: ToolContext,
    workspace_unique_id: Annotated[
        str, "Globally unique identifier for the workspace or organization in Asana."
    ],
    enable_pretty_output: Annotated[
        bool | None,
        'Set to true for a "pretty" response with indentation and line breaks. Recommended for debugging as it increases response size and time.',  # noqa: E501
    ] = None,
    include_optional_fields: Annotated[
        list[str] | None,
        "List properties to include in the response. Provide these as an array of strings to retrieve additional user information beyond the default fields.",  # noqa: E501
    ] = None,
    pagination_offset_token: Annotated[
        str | None,
        "An offset token used for paginating results. It allows you to retrieve the next page of results by using a token returned from a previous request. If omitted, the first page is returned.",  # noqa: E501
    ] = None,
) -> Annotated[dict[str, Any], "Response from the API endpoint 'getUsersForWorkspace'."]:
    """Retrieve all users from a specified workspace.

    Fetches compact records for all users in a specific Asana workspace or organization. Suitable for retrieving member lists sorted alphabetically, limited to 2000 users."""  # noqa: E501
    request_data: Any = None
    if isinstance(request_data, dict):
        request_data = remove_none_values(request_data)
    content = json.dumps(request_data) if request_data else None
    response = await make_request(
        url="https://app.asana.com/api/1.0/workspaces/{workspace_gid}/users".format(  # noqa: UP032
            workspace_gid=workspace_unique_id
        ),
        method="GET",
        params=remove_none_values({
            "opt_pretty": enable_pretty_output,
            "offset": pagination_offset_token,
            "opt_fields": include_optional_fields,
        }),
        headers=remove_none_values({
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            )
        }),
        content=content,
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(requires_auth=OAuth2(id="arcade-asana", scopes=["webhooks:read"]))
async def get_asana_webhooks(
    context: ToolContext,
    workspace_id: Annotated[
        str, "The unique identifier of the Asana workspace to query for webhooks."
    ],
    enable_pretty_output: Annotated[
        bool | None,
        "Set to true for a readable, pretty-formatted JSON output. Increases response time and size. Use mainly for debugging.",  # noqa: E501
    ] = None,
    include_optional_properties: Annotated[
        list[str] | None,
        "Comma-separated list of properties to include in the response, which are excluded by default.",  # noqa: E501
    ] = None,
    pagination_offset_token: Annotated[
        str | None,
        "An offset token to retrieve the next page of results. Use the token from a previous API response for pagination.",  # noqa: E501
    ] = None,
    results_per_page: Annotated[
        int | None, "Number of webhooks to return per page, between 1 and 100."
    ] = None,
    specific_resource: Annotated[
        str | None, "Specify the resource ID to filter webhooks for that particular resource."
    ] = None,
) -> Annotated[dict[str, Any], "Response from the API endpoint 'getWebhooks'."]:
    """Retrieve all registered Asana webhooks for a workspace.

    Use this tool to get a compact representation of all webhooks registered by your app for the authenticated user in a specified Asana workspace. Requires the 'webhooks:read' scope."""  # noqa: E501
    request_data: Any = None
    if isinstance(request_data, dict):
        request_data = remove_none_values(request_data)
    content = json.dumps(request_data) if request_data else None
    response = await make_request(
        url="https://app.asana.com/api/1.0/webhooks",
        method="GET",
        params=remove_none_values({
            "opt_pretty": enable_pretty_output,
            "limit": results_per_page,
            "offset": pagination_offset_token,
            "workspace": workspace_id,
            "resource": specific_resource,
            "opt_fields": include_optional_properties,
        }),
        headers=remove_none_values({
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            )
        }),
        content=content,
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(requires_auth=OAuth2(id="arcade-asana", scopes=["webhooks:write"]))
async def create_asana_webhook(
    context: ToolContext,
    mode: Annotated[
        ToolMode,
        "Operation mode: 'get_request_schema' returns the OpenAPI spec "
        "for the request body, 'execute' performs the actual operation",
    ],
    include_optional_properties: Annotated[
        list[str] | None,
        "Specify properties to include in the response. Provide a list of property names to include those optional properties in the response.  Only used when mode is 'execute'.",  # noqa: E501
    ] = None,
    enable_pretty_output: Annotated[
        bool | None,
        "If true, provides the response in a pretty format with line breaks and indentation. Use it for debugging as it increases response time and size.  Only used when mode is 'execute'.",  # noqa: E501
    ] = None,
    request_body: Annotated[
        str | None,
        "Stringified JSON representing the request body. Required when "
        "mode is 'execute', ignored when mode is 'get_request_schema'",
    ] = None,
) -> Annotated[dict[str, Any], "Response from the API endpoint 'createWebhook'."]:
    """Initiates the creation of a webhook in Asana.

    This tool initiates the creation of a webhook in Asana and requires the 'webhooks:write' scope. It involves a confirmation handshake for setup. Ensure your server can handle asynchronous requests to acknowledge the handshake for successful creation. Invalid hostnames like localhost are not allowed.

    Note: Understanding the request schema is necessary to properly create
    the stringified JSON input object for execution.\n\nThis operation also requires query parameters.

    Modes:
    - GET_REQUEST_SCHEMA: Returns the schema. Only call if you don't
      already have it. Do NOT call repeatedly if you already received
      the schema.
    - EXECUTE: Performs the operation with the provided request body
      JSON.\n      Note: You must also provide the required query parameters when executing.

    If you need the schema, call with mode='get_request_schema' ONCE, then execute.
    """  # noqa: E501
    if mode == ToolMode.GET_REQUEST_SCHEMA:
        return {
            "request_body_schema": REQUEST_BODY_SCHEMAS["CREATEASANAWEBHOOK"],
            "instructions": (
                "Use the request_body_schema to construct a valid JSON object. "
                "Once you have populated the object following the schema "
                "structure and requirements, call this tool again with "
                "mode='execute' and the stringified JSON as the "
                "request_body parameter along with the required query parameters. "
                "Do NOT call the schema mode again - you already have "
                "the schema now."
            ),
        }

    # Mode is EXECUTE - validate parameters

    # Validate request body is provided (not None or empty string)
    # Note: Empty objects like {} are allowed - schema validation will check if valid
    if request_body is None or request_body.strip() == "":
        raise RetryableToolError(
            message="Request body is required when mode is 'execute'",
            developer_message="The request_body parameter was null or empty string",
            additional_prompt_content=(
                "The request body is required to perform this operation. "
                "Use the schema below to construct a valid JSON object, "
                "then call this tool again in execute mode with the "
                "stringified JSON as the request_body parameter.\n\n"
                "Schema:\n\n```json\n" + REQUEST_BODY_SCHEMAS["CREATEASANAWEBHOOK"] + "\n```"
            ),
        )

    # Parse JSON
    try:
        request_data = json.loads(request_body)
    except json.JSONDecodeError as e:
        raise RetryableToolError(
            message=f"Invalid JSON in request body: {e!s}",
            developer_message=f"JSON parsing failed: {e!s}",
            additional_prompt_content=(
                f"The request body contains invalid JSON. Error: {e!s}\n\n"
                "Please provide a valid JSON string that matches the schema "
                "below, then call this tool again in execute mode.\n\n"
                "Schema:\n\n```json\n" + REQUEST_BODY_SCHEMAS["CREATEASANAWEBHOOK"] + "\n```"
            ),
        ) from e

    response = await make_request_with_schema_validation(
        url="https://app.asana.com/api/1.0/webhooks",
        method="POST",
        request_data=request_data,
        schema=REQUEST_BODY_SCHEMAS["CREATEASANAWEBHOOK"],
        params=remove_none_values({
            "opt_pretty": enable_pretty_output,
            "opt_fields": include_optional_properties,
        }),
        headers=remove_none_values({
            "Content-Type": "application/json",
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            ),
        }),
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(requires_auth=OAuth2(id="arcade-asana", scopes=["webhooks:read"]))
async def retrieve_webhook_details(
    context: ToolContext,
    webhook_global_identifier: Annotated[
        str, "Globally unique identifier for the webhook to retrieve its full record."
    ],
    enable_pretty_output: Annotated[
        bool | None,
        "Enables pretty output formatting with line breaks and indentation. Use this for debugging, but be aware it increases response time and size.",  # noqa: E501
    ] = None,
    included_fields: Annotated[
        list[str] | None,
        "List of optional properties to include in the response. Provide these as an array of strings.",  # noqa: E501
    ] = None,
) -> Annotated[dict[str, Any], "Response from the API endpoint 'getWebhook'."]:
    """Retrieve the full record of a specified webhook.

    Use this tool to get complete details of a webhook by its ID. Requires webhooks:read scope."""
    request_data: Any = None
    if isinstance(request_data, dict):
        request_data = remove_none_values(request_data)
    content = json.dumps(request_data) if request_data else None
    response = await make_request(
        url="https://app.asana.com/api/1.0/webhooks/{webhook_gid}".format(  # noqa: UP032
            webhook_gid=webhook_global_identifier
        ),
        method="GET",
        params=remove_none_values({
            "opt_pretty": enable_pretty_output,
            "opt_fields": included_fields,
        }),
        headers=remove_none_values({
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            )
        }),
        content=content,
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(requires_auth=OAuth2(id="arcade-asana", scopes=["webhooks:write"]))
async def asana_update_webhook_filters(
    context: ToolContext,
    mode: Annotated[
        ToolMode,
        "Operation mode: 'get_request_schema' returns the OpenAPI spec "
        "for the request body, 'execute' performs the actual operation",
    ],
    webhook_global_id: Annotated[
        str | None,
        "Globally unique identifier for the webhook to update its filters.  Required when mode is 'execute', ignored when mode is 'get_request_schema'.",  # noqa: E501
    ] = None,
    include_optional_properties: Annotated[
        list[str] | None,
        "Comma-separated list of optional properties to include in the response.  Only used when mode is 'execute'.",  # noqa: E501
    ] = None,
    enable_pretty_output: Annotated[
        bool | None,
        "Enable this to receive the response in a human-readable format with line breaking and indentation. Recommended only for debugging as it increases response size and time.  Only used when mode is 'execute'.",  # noqa: E501
    ] = None,
    request_body: Annotated[
        str | None,
        "Stringified JSON representing the request body. Required when "
        "mode is 'execute', ignored when mode is 'get_request_schema'",
    ] = None,
) -> Annotated[dict[str, Any], "Response from the API endpoint 'updateWebhook'."]:
    """Update filters for an Asana webhook.

    Use this tool to update the filters of an existing Asana webhook by providing new filter specifications. The existing filters will be completely replaced with the new ones provided.

    Note: Understanding the request schema is necessary to properly create
    the stringified JSON input object for execution.\n\nThis operation also requires path, query parameters.

    Modes:
    - GET_REQUEST_SCHEMA: Returns the schema. Only call if you don't
      already have it. Do NOT call repeatedly if you already received
      the schema.
    - EXECUTE: Performs the operation with the provided request body
      JSON.\n      Note: You must also provide the required path, query parameters when executing.

    If you need the schema, call with mode='get_request_schema' ONCE, then execute.
    """  # noqa: E501
    if mode == ToolMode.GET_REQUEST_SCHEMA:
        return {
            "request_body_schema": REQUEST_BODY_SCHEMAS["ASANAUPDATEWEBHOOKFILTERS"],
            "instructions": (
                "Use the request_body_schema to construct a valid JSON object. "
                "Once you have populated the object following the schema "
                "structure and requirements, call this tool again with "
                "mode='execute' and the stringified JSON as the "
                "request_body parameter along with the required path, query parameters. "
                "Do NOT call the schema mode again - you already have "
                "the schema now."
            ),
        }

    # Mode is EXECUTE - validate parameters
    # Validate required parameters
    missing_params = []
    if not webhook_global_id:
        missing_params.append(("webhook_global_id", "path"))

    if missing_params:
        param_names = [p[0] for p in missing_params]
        param_details = ", ".join([f"{p[0]} ({p[1]})" for p in missing_params])
        raise RetryableToolError(
            message=f"Missing required parameters: {param_names}",
            developer_message=(f"Required parameters validation failed: {param_details}"),
            additional_prompt_content=(
                f"The following required parameters are missing: "
                f"{param_details}. Please call this tool again with all "
                "required parameters."
            ),
        )

    # Validate request body is provided (not None or empty string)
    # Note: Empty objects like {} are allowed - schema validation will check if valid
    if request_body is None or request_body.strip() == "":
        raise RetryableToolError(
            message="Request body is required when mode is 'execute'",
            developer_message="The request_body parameter was null or empty string",
            additional_prompt_content=(
                "The request body is required to perform this operation. "
                "Use the schema below to construct a valid JSON object, "
                "then call this tool again in execute mode with the "
                "stringified JSON as the request_body parameter.\n\n"
                "Schema:\n\n```json\n" + REQUEST_BODY_SCHEMAS["ASANAUPDATEWEBHOOKFILTERS"] + "\n```"
            ),
        )

    # Parse JSON
    try:
        request_data = json.loads(request_body)
    except json.JSONDecodeError as e:
        raise RetryableToolError(
            message=f"Invalid JSON in request body: {e!s}",
            developer_message=f"JSON parsing failed: {e!s}",
            additional_prompt_content=(
                f"The request body contains invalid JSON. Error: {e!s}\n\n"
                "Please provide a valid JSON string that matches the schema "
                "below, then call this tool again in execute mode.\n\n"
                "Schema:\n\n```json\n" + REQUEST_BODY_SCHEMAS["ASANAUPDATEWEBHOOKFILTERS"] + "\n```"
            ),
        ) from e

    response = await make_request_with_schema_validation(
        url="https://app.asana.com/api/1.0/webhooks/{webhook_gid}".format(  # noqa: UP032
            webhook_gid=webhook_global_id
        ),
        method="PUT",
        request_data=request_data,
        schema=REQUEST_BODY_SCHEMAS["ASANAUPDATEWEBHOOKFILTERS"],
        params=remove_none_values({
            "opt_pretty": enable_pretty_output,
            "opt_fields": include_optional_properties,
        }),
        headers=remove_none_values({
            "Content-Type": "application/json",
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            ),
        }),
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(requires_auth=OAuth2(id="arcade-asana", scopes=["webhooks:delete"]))
async def delete_asana_webhook(
    context: ToolContext,
    webhook_global_id: Annotated[
        str, "Globally unique identifier for the specific webhook to delete in Asana."
    ],
    enable_pretty_output: Annotated[
        bool | None,
        "If true, formats the response with line breaks and indentation for readability. Use primarily for debugging as it increases response time and size.",  # noqa: E501
    ] = None,
) -> Annotated[dict[str, Any], "Response from the API endpoint 'deleteWebhook'."]:
    """Permanently delete a webhook in Asana.

    Use this tool to permanently remove a webhook from Asana. Even after deletion, requests that were in flight may still be received. No further requests will be issued once the webhook is deleted. Ensure you have the required 'webhooks:delete' scope."""  # noqa: E501
    request_data: Any = None
    if isinstance(request_data, dict):
        request_data = remove_none_values(request_data)
    content = json.dumps(request_data) if request_data else None
    response = await make_request(
        url="https://app.asana.com/api/1.0/webhooks/{webhook_gid}".format(  # noqa: UP032
            webhook_gid=webhook_global_id
        ),
        method="DELETE",
        params=remove_none_values({"opt_pretty": enable_pretty_output}),
        headers=remove_none_values({
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            )
        }),
        content=content,
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(requires_auth=OAuth2(id="arcade-asana"))
async def get_workspace_membership(
    context: ToolContext,
    workspace_membership_id: Annotated[
        str, "The unique identifier for the workspace membership to retrieve."
    ],
    enable_pretty_output: Annotated[
        bool | None,
        'Set to true for "pretty" JSON formatting with line breaks and indentation. Increases response time and size; use for debugging.',  # noqa: E501
    ] = None,
    include_optional_properties: Annotated[
        list[str] | None,
        "List of specific properties to include in the response, as some are excluded by default.",
    ] = None,
) -> Annotated[dict[str, Any], "Response from the API endpoint 'getWorkspaceMembership'."]:
    """Get the complete record for a workspace membership.

    Use this tool to retrieve detailed information about a specific workspace membership in Asana."""  # noqa: E501
    request_data: Any = None
    if isinstance(request_data, dict):
        request_data = remove_none_values(request_data)
    content = json.dumps(request_data) if request_data else None
    response = await make_request(
        url="https://app.asana.com/api/1.0/workspace_memberships/{workspace_membership_gid}".format(  # noqa: UP032
            workspace_membership_gid=workspace_membership_id
        ),
        method="GET",
        params=remove_none_values({
            "opt_pretty": enable_pretty_output,
            "opt_fields": include_optional_properties,
        }),
        headers=remove_none_values({
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            )
        }),
        content=content,
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(requires_auth=OAuth2(id="arcade-asana"))
async def get_user_workspace_memberships(
    context: ToolContext,
    user_identifier: Annotated[
        str, "String identifying a user. Use 'me', an email, or a user gid."
    ],
    enable_pretty_output: Annotated[
        bool | None,
        "If true, provides the response in a readable, pretty format with line breaks and indentation. Useful for debugging.",  # noqa: E501
    ] = None,
    include_optional_fields: Annotated[
        list[str] | None,
        "Comma-separated list of optional user properties to include in the response.",
    ] = None,
    page_offset_token: Annotated[
        str | None,
        "Token to fetch the next page of results from a paginated request. Use a token returned from a previous response.",  # noqa: E501
    ] = None,
    results_per_page: Annotated[
        int | None, "Number of objects to return per page, between 1 and 100."
    ] = None,
) -> Annotated[dict[str, Any], "Response from the API endpoint 'getWorkspaceMembershipsForUser'."]:
    """Fetches a user's workspace membership records in Asana.

    Use this tool to obtain compact workspace membership details for a specific user in Asana. Call this tool when you need to understand which workspaces a user is associated with."""  # noqa: E501
    request_data: Any = None
    if isinstance(request_data, dict):
        request_data = remove_none_values(request_data)
    content = json.dumps(request_data) if request_data else None
    response = await make_request(
        url="https://app.asana.com/api/1.0/users/{user_gid}/workspace_memberships".format(  # noqa: UP032
            user_gid=user_identifier
        ),
        method="GET",
        params=remove_none_values({
            "opt_pretty": enable_pretty_output,
            "limit": results_per_page,
            "offset": page_offset_token,
            "opt_fields": include_optional_fields,
        }),
        headers=remove_none_values({
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            )
        }),
        content=content,
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(requires_auth=OAuth2(id="arcade-asana"))
async def get_workspace_memberships(
    context: ToolContext,
    workspace_global_id: Annotated[
        str,
        "A globally unique identifier for the workspace or organization to fetch memberships for.",
    ],
    enable_pretty_output: Annotated[
        bool | None, "Enable pretty output with line breaks and indentation for debugging purposes."
    ] = None,
    include_optional_fields: Annotated[
        list[str] | None,
        "An array of strings specifying optional properties to include in the response.",
    ] = None,
    pagination_offset: Annotated[
        str | None,
        "Offset token for pagination to retrieve the next page of results. Use the token returned from a previous call for continuous paging, or leave empty for the first page.",  # noqa: E501
    ] = None,
    results_per_page: Annotated[
        int | None, "Specify the number of membership records to return per page, from 1 to 100."
    ] = None,
    user_identifier: Annotated[
        str | None, "A string to identify a user, such as 'me', an email, or a user's gid in Asana."
    ] = None,
) -> Annotated[
    dict[str, Any], "Response from the API endpoint 'getWorkspaceMembershipsForWorkspace'."
]:
    """Retrieve workspace membership records.

    This tool calls Asana's API to fetch compact workspace membership records for a specified workspace. It should be used when you need to list or check members of a workspace."""  # noqa: E501
    request_data: Any = None
    if isinstance(request_data, dict):
        request_data = remove_none_values(request_data)
    content = json.dumps(request_data) if request_data else None
    response = await make_request(
        url="https://app.asana.com/api/1.0/workspaces/{workspace_gid}/workspace_memberships".format(  # noqa: UP032
            workspace_gid=workspace_global_id
        ),
        method="GET",
        params=remove_none_values({
            "user": user_identifier,
            "opt_pretty": enable_pretty_output,
            "limit": results_per_page,
            "offset": pagination_offset,
            "opt_fields": include_optional_fields,
        }),
        headers=remove_none_values({
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            )
        }),
        content=content,
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(requires_auth=OAuth2(id="arcade-asana", scopes=["workspaces:read"]))
async def get_visible_workspaces(
    context: ToolContext,
    enable_pretty_output: Annotated[
        bool | None,
        "Enable for pretty, readable response. Ideal for debugging as it increases response time and size.",  # noqa: E501
    ] = None,
    include_optional_properties: Annotated[
        list[str] | None,
        "Comma-separated list of optional fields to include in the response. Use this to include properties that are excluded by default.",  # noqa: E501
    ] = None,
    pagination_offset: Annotated[
        str | None,
        "An offset token for paginating through the results. Use the token returned from a previous request to access the next page.",  # noqa: E501
    ] = None,
    results_limit_per_page: Annotated[
        int | None,
        "Specify the number of workspaces to return per page. Must be between 1 and 100.",
    ] = None,
) -> Annotated[dict[str, Any], "Response from the API endpoint 'getWorkspaces'."]:
    """Retrieve all workspaces visible to the user.

    Fetches a list of compact records for all workspaces the authorized user can see, requiring the 'workspaces:read' scope."""  # noqa: E501
    request_data: Any = None
    if isinstance(request_data, dict):
        request_data = remove_none_values(request_data)
    content = json.dumps(request_data) if request_data else None
    response = await make_request(
        url="https://app.asana.com/api/1.0/workspaces",
        method="GET",
        params=remove_none_values({
            "opt_pretty": enable_pretty_output,
            "limit": results_limit_per_page,
            "offset": pagination_offset,
            "opt_fields": include_optional_properties,
        }),
        headers=remove_none_values({
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            )
        }),
        content=content,
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(requires_auth=OAuth2(id="arcade-asana", scopes=["workspaces:read"]))
async def get_workspace_details(
    context: ToolContext,
    workspace_global_id: Annotated[
        str, "Globally unique identifier for the Asana workspace or organization."
    ],
    enable_pretty_output: Annotated[
        bool | None,
        "Enable this to provide a response with pretty formatting, including line breaks and indentation. Recommended for debugging due to increased response size and time.",  # noqa: E501
    ] = None,
    include_optional_properties: Annotated[
        list[str] | None,
        "List the optional properties to include, as a comma-separated list, to extend the default fields in the workspace record.",  # noqa: E501
    ] = None,
) -> Annotated[dict[str, Any], "Response from the API endpoint 'getWorkspace'."]:
    """Retrieve detailed information about a specific Asana workspace.

    Use this tool to get the full record of a specific workspace in Asana by providing the workspace ID. It requires the 'workspaces:read' scope."""  # noqa: E501
    request_data: Any = None
    if isinstance(request_data, dict):
        request_data = remove_none_values(request_data)
    content = json.dumps(request_data) if request_data else None
    response = await make_request(
        url="https://app.asana.com/api/1.0/workspaces/{workspace_gid}".format(  # noqa: UP032
            workspace_gid=workspace_global_id
        ),
        method="GET",
        params=remove_none_values({
            "opt_pretty": enable_pretty_output,
            "opt_fields": include_optional_properties,
        }),
        headers=remove_none_values({
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            )
        }),
        content=content,
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(requires_auth=OAuth2(id="arcade-asana"))
async def update_workspace_name(
    context: ToolContext,
    workspace_global_id: Annotated[
        str, "Globally unique identifier for the workspace or organization."
    ],
    enable_pretty_output: Annotated[
        bool | None,
        "Enable this to receive the response in a readable and formatted style. Useful for debugging. May increase response size and time.",  # noqa: E501
    ] = None,
    include_optional_fields: Annotated[
        list[str] | None,
        "List of additional workspace properties to include in the response, comma-separated.",
    ] = None,
    resource_base_type: Annotated[
        str | None, "Specify the base type of the resource to update the workspace name."
    ] = None,
    workspace_name: Annotated[
        str | None, "The new name for the Asana workspace you want to update."
    ] = None,
    workspace_resource_gid: Annotated[
        str | None,
        "Globally unique identifier of the workspace resource. This is required to specify which workspace to update.",  # noqa: E501
    ] = None,
) -> Annotated[dict[str, Any], "Response from the API endpoint 'updateWorkspace'."]:
    """Update the name of an existing Asana workspace.

    Use this tool to change the name of a specific, existing Asana workspace. Only the workspace name can be modified, and the tool will return the complete, updated workspace record."""  # noqa: E501
    request_data: Any = {
        "data": {
            "gid": workspace_resource_gid,
            "resource_type": resource_base_type,
            "name": workspace_name,
        }
    }
    if isinstance(request_data, dict):
        request_data = remove_none_values(request_data)
    content = json.dumps(request_data) if request_data else None
    response = await make_request(
        url="https://app.asana.com/api/1.0/workspaces/{workspace_gid}".format(  # noqa: UP032
            workspace_gid=workspace_global_id
        ),
        method="PUT",
        params=remove_none_values({
            "opt_pretty": enable_pretty_output,
            "opt_fields": include_optional_fields,
        }),
        headers=remove_none_values({
            "Content-Type": "application/json",
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            ),
        }),
        content=content,
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(requires_auth=OAuth2(id="arcade-asana"))
async def add_user_to_workspace(
    context: ToolContext,
    workspace_global_identifier: Annotated[
        str, "Globally unique identifier for the workspace or organization in Asana."
    ],
    enable_pretty_output: Annotated[
        bool | None,
        "Enable this to receive the response in a readable JSON format with line breaks and indentation. This is useful for debugging but can increase response time and size.",  # noqa: E501
    ] = None,
    include_optional_fields: Annotated[
        list[str] | None, "Comma-separated list of optional properties to include in the response."
    ] = None,
    user_identifier: Annotated[
        str | None, 'A string identifying a user. Can be "me", an email, or a user ID (gid).'
    ] = None,
) -> Annotated[dict[str, Any], "Response from the API endpoint 'addUserForWorkspace'."]:
    """Add a user to an Asana workspace or organization.

    Use this tool to add a user to an Asana workspace. The user can be specified by their unique user ID or email. It returns the complete user record of the invited user."""  # noqa: E501
    request_data: Any = {"data": {"user": user_identifier}}
    if isinstance(request_data, dict):
        request_data = remove_none_values(request_data)
    content = json.dumps(request_data) if request_data else None
    response = await make_request(
        url="https://app.asana.com/api/1.0/workspaces/{workspace_gid}/addUser".format(  # noqa: UP032
            workspace_gid=workspace_global_identifier
        ),
        method="POST",
        params=remove_none_values({
            "opt_pretty": enable_pretty_output,
            "opt_fields": include_optional_fields,
        }),
        headers=remove_none_values({
            "Content-Type": "application/json",
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            ),
        }),
        content=content,
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(requires_auth=OAuth2(id="arcade-asana"))
async def remove_user_from_workspace(
    context: ToolContext,
    workspace_identifier: Annotated[
        str, "Globally unique identifier for the workspace or organization in Asana."
    ],
    enable_pretty_output: Annotated[
        bool | None,
        "Enable to receive formatted and more readable output. Use for debugging due to increased response size and time.",  # noqa: E501
    ] = None,
    user_identifier: Annotated[
        str | None,
        "Identifies the user to be removed. Accepts 'me', an email, or a globally unique user ID.",
    ] = None,
) -> Annotated[dict[str, Any], "Response from the API endpoint 'removeUserForWorkspace'."]:
    """Remove a user from an Asana workspace or organization.

    Use this tool to remove a user from a specified workspace or organization in Asana. The caller must be an admin in the workspace. The user can be identified by their user ID or email. Different token types (Service Account Token or Personal Access Token) affect post-removal behaviors, such as resource ownership transfer."""  # noqa: E501
    request_data: Any = {"data": {"user": user_identifier}}
    if isinstance(request_data, dict):
        request_data = remove_none_values(request_data)
    content = json.dumps(request_data) if request_data else None
    response = await make_request(
        url="https://app.asana.com/api/1.0/workspaces/{workspace_gid}/removeUser".format(  # noqa: UP032
            workspace_gid=workspace_identifier
        ),
        method="POST",
        params=remove_none_values({"opt_pretty": enable_pretty_output}),
        headers=remove_none_values({
            "Content-Type": "application/json",
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            ),
        }),
        content=content,
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(requires_auth=OAuth2(id="arcade-asana"))
async def get_workspace_events(
    context: ToolContext,
    workspace_global_id: Annotated[
        str, "Globally unique identifier for the workspace or organization in Asana."
    ],
    enable_pretty_output: Annotated[
        bool | None,
        "Enable pretty output for better readability. Recommended for debugging as it increases response size and takes extra time.",  # noqa: E501
    ] = None,
    sync_token: Annotated[
        str | None,
        "A sync token received from the last request to fetch events from a specific point in time. Omit on first sync to receive a new token.",  # noqa: E501
    ] = None,
) -> Annotated[dict[str, Any], "Response from the API endpoint 'getWorkspaceEvents'."]:
    """Retrieve all events in a workspace since a specific sync token.

    Use this tool to get a full record of events that have occurred in an Asana workspace since a specified sync token. If the number of events exceeds 1000, the response will indicate there are more events to retrieve."""  # noqa: E501
    request_data: Any = None
    if isinstance(request_data, dict):
        request_data = remove_none_values(request_data)
    content = json.dumps(request_data) if request_data else None
    response = await make_request(
        url="https://app.asana.com/api/1.0/workspaces/{workspace_gid}/events".format(  # noqa: UP032
            workspace_gid=workspace_global_id
        ),
        method="GET",
        params=remove_none_values({"opt_pretty": enable_pretty_output, "sync": sync_token}),
        headers=remove_none_values({
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            )
        }),
        content=content,
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}
