Repository: sooperset/mcp-atlassian

Issue:
[Bug]: labels are not added
### Prerequisites

- [x] I have searched the [existing issues](https://github.com/sooperset/mcp-atlassian/issues) to make sure this bug has not already been reported.
- [x] I have checked the [README](https://github.com/sooperset/mcp-atlassian/blob/main/README.md) for relevant information.

### Bug Description

Put the following in windsurf 1.7 with Cloude 3.7 Sonnet model

Create JIRA task in project USAWS
type: epic
summary: create SSO integration for infrastructure components
body: create SSO integration for infrastructure components
labels: devops,sso

It created the epic, but was not able to create labels

then it tried to fix it and recreated a bunch of epics

![Image](https://github.com/user-attachments/assets/19beba57-a9bb-4faf-9b14-55e1c33c17aa)


### Steps to Reproduce

Create JIRA task in project USAWS
type: epic
summary: create SSO integration for infrastructure components
body: create SSO integration for infrastructure components
labels: devops,sso


### Expected Behavior

issue with labels to be created

### Actual Behavior

```shell
labels are not created
```

### mcp-atlassian Version

latest

### Installation Method

Docker

### Operating System

Linux (Specify distribution below if relevant)

### Python Version

Fedora 41

### Atlassian Instance Type

Jira Cloud

### Client Application

Windsurf 1.7

### Additional Context

_No response_

Discussion:
@sooperset: Hi, @dmitry-mightydevops. Thanks for reporting! Confirmed labels are not added when creating tickets. Let me look into this issue

Solution patch:
diff --git a/src/mcp_atlassian/jira/constants.py b/src/mcp_atlassian/jira/constants.py
new file mode 100644
index 0000000..7be6b80
--- /dev/null
+++ b/src/mcp_atlassian/jira/constants.py
@@ -0,0 +1,15 @@
+"""Constants specific to Jira operations."""
+
+# Set of default fields returned by Jira read operations when no specific fields are requested.
+DEFAULT_READ_JIRA_FIELDS: set[str] = {
+    "summary",
+    "description",
+    "status",
+    "assignee",
+    "reporter",
+    "labels",
+    "priority",
+    "created",
+    "updated",
+    "issuetype",
+}
diff --git a/src/mcp_atlassian/jira/fields.py b/src/mcp_atlassian/jira/fields.py
index 12534de..622e5cf 100644
--- a/src/mcp_atlassian/jira/fields.py
+++ b/src/mcp_atlassian/jira/fields.py
@@ -19,6 +19,8 @@ class FieldsMixin(JiraClient, EpicOperationsProto, UsersOperationsProto):
     different Jira instances, especially for custom fields.
     """
 
+    _field_name_to_id_map: dict[str, str] | None = None  # Cache for name -> id mapping
+
     def get_fields(self, refresh: bool = False) -> list[dict[str, Any]]:
         """
         Get all available fields from Jira.
@@ -34,6 +36,11 @@ class FieldsMixin(JiraClient, EpicOperationsProto, UsersOperationsProto):
             if self._field_ids_cache is not None and not refresh:
                 return self._field_ids_cache
 
+            if refresh:
+                self._field_name_to_id_map = (
+                    None  # Clear name map cache if refreshing fields
+                )
+
             # Fetch fields from Jira API
             fields = self.jira.get_all_fields()
             if not isinstance(fields, list):
@@ -44,6 +51,9 @@ class FieldsMixin(JiraClient, EpicOperationsProto, UsersOperationsProto):
             # Cache the fields
             self._field_ids_cache = fields
 
+            # Regenerate the name map upon fetching new fields
+            self._generate_field_map(force_regenerate=True)
+
             # Log available fields for debugging
             self._log_available_fields(fields)
 
@@ -53,6 +63,37 @@ class FieldsMixin(JiraClient, EpicOperationsProto, UsersOperationsProto):
             logger.error(f"Error getting Jira fields: {str(e)}")
             return []
 
+    def _generate_field_map(self, force_regenerate: bool = False) -> dict[str, str]:
+        """Generates and caches a map of lowercase field names to field IDs."""
+        if self._field_name_to_id_map is not None and not force_regenerate:
+            return self._field_name_to_id_map
+
+        # Ensure fields are loaded into cache first
+        fields = (
+            self.get_fields()
+        )  # Uses cache if available unless force_regenerate was True
+        if not fields:
+            self._field_name_to_id_map = {}
+            return {}
+
+        name_map: dict[str, str] = {}
+        id_map: dict[str, str] = {}  # Also map ID to ID for consistency
+        for field in fields:
+            field_id = field.get("id")
+            field_name = field.get("name")
+            if field_id:
+                id_map[field_id] = field_id  # Map ID to itself
+                if field_name:
+                    # Store lowercase name -> ID. Handle potential name collisions if necessary.
+                    name_map.setdefault(field_name.lower(), field_id)
+
+        # Combine maps, ensuring IDs can also be looked up directly
+        self._field_name_to_id_map = name_map | id_map
+        logger.debug(
+            f"Generated/Updated field name map: {len(self._field_name_to_id_map)} entries"
+        )
+        return self._field_name_to_id_map
+
     def get_field_id(self, field_name: str, refresh: bool = False) -> str | None:
         """
         Get the ID for a specific field by name.
@@ -65,28 +106,21 @@ class FieldsMixin(JiraClient, EpicOperationsProto, UsersOperationsProto):
             Field ID if found, None otherwise
         """
         try:
-            # Normalize the field name to lowercase for case-insensitive matching
-            normalized_name = field_name.lower()
-
-            # Get all fields and search for the requested field
-            fields = self.get_fields(refresh=refresh)
+            # Ensure the map is generated/cached
+            field_map = self._generate_field_map(force_regenerate=refresh)
+            if not field_map:
+                logger.error("Field map could not be generated.")
+                return None
 
-            for field in fields:
-                name = field.get("name", "")
-                if name and name.lower() == normalized_name:
-                    return field.get("id")
-
-            # If not found by exact match, try partial match
-            for field in fields:
-                name = field.get("name", "")
-                if name and normalized_name in name.lower():
-                    logger.info(
-                        f"Found field '{name}' as partial match for '{field_name}'"
-                    )
-                    return field.get("id")
-
-            logger.warning(f"Field '{field_name}' not found")
-            return None
+            normalized_name = field_name.lower()
+            if normalized_name in field_map:
+                return field_map[normalized_name]
+            # Fallback: Check if the input IS an ID (using original casing)
+            elif field_name in field_map:  # Checks the id_map part
+                return field_map[field_name]
+            else:
+                logger.warning(f"Field '{field_name}' not found in generated map.")
+                return None
 
         except Exception as e:
             logger.error(f"Error getting field ID for '{field_name}': {str(e)}")
@@ -222,16 +256,17 @@ class FieldsMixin(JiraClient, EpicOperationsProto, UsersOperationsProto):
             (e.g., {'epic_link': 'customfield_10014', 'epic_name': 'customfield_10011'})
         """
         try:
-            # Check if we've already cached the field IDs
-            if self._field_ids_cache:
-                result = {field["name"]: field["id"] for field in self._field_ids_cache}
-                return result
-
-            # Initialize cache if needed
-            self._field_ids_cache = []
+            # Ensure field list and map are cached/generated
+            self._generate_field_map()  # Generates map and ensures fields are cached
 
-            # Fetch all fields from Jira API
+            # Get all fields (uses cache if available)
             fields = self.get_fields()
+            if not fields:  # Check if get_fields failed or returned empty
+                logger.error(
+                    "Could not load field definitions for epic field discovery."
+                )
+                return {}
+
             field_ids = {}
 
             # Log the complete list of fields for debugging
@@ -246,10 +281,6 @@ class FieldsMixin(JiraClient, EpicOperationsProto, UsersOperationsProto):
             }
             logger.debug(f"Custom fields: {custom_fields}")
 
-            # Get all fields
-            fields = self.get_fields()
-            field_ids = {}
-
             # Look for Epic-related fields - use multiple strategies to identify them
             for field in fields:
                 field_name = field.get("name", "").lower()
@@ -328,12 +359,6 @@ class FieldsMixin(JiraClient, EpicOperationsProto, UsersOperationsProto):
                         f"Found potential Epic-related field: {field_id} ({original_name})"
                     )
 
-            # Cache the results for future use
-            self._field_ids_cache = [
-                {"id": field_id, "name": field_name}
-                for field_name, field_id in field_ids.items()
-            ]
-
             # If we couldn't find certain key fields, try alternative approaches
             if "epic_name" not in field_ids or "epic_link" not in field_ids:
                 logger.debug(
diff --git a/src/mcp_atlassian/jira/issues.py b/src/mcp_atlassian/jira/issues.py
index 107ceeb..cae5e9d 100644
--- a/src/mcp_atlassian/jira/issues.py
+++ b/src/mcp_atlassian/jira/issues.py
@@ -11,6 +11,7 @@ from ..models.jira import JiraIssue
 from ..models.jira.common import JiraChangelog
 from ..utils import parse_date
 from .client import JiraClient
+from .constants import DEFAULT_READ_JIRA_FIELDS
 from .protocols import (
     AttachmentsOperationsProto,
     EpicOperationsProto,
@@ -37,11 +38,7 @@ class IssuesMixin(
         issue_key: str,
         expand: str | None = None,
         comment_limit: int | str | None = 10,
-        fields: str
-        | list[str]
-        | tuple[str, ...]
-        | set[str]
-        | None = "summary,description,status,assignee,reporter,labels,priority,created,updated,issuetype",
+        fields: str | list[str] | tuple[str, ...] | set[str] | None = None,
         properties: str | list[str] | None = None,
         update_history: bool = True,
     ) -> JiraIssue:
@@ -64,14 +61,24 @@ class IssuesMixin(
             Exception: If there is an error retrieving the issue
         """
         try:
+            # Determine fields_param: use provided fields or default from constant
+            fields_param = fields
+            if fields_param is None:
+                fields_param = ",".join(DEFAULT_READ_JIRA_FIELDS)
+            elif isinstance(fields_param, list | tuple | set):
+                fields_param = ",".join(fields_param)
+
             # Ensure necessary fields are included based on special parameters
             if (
-                isinstance(fields, str)
-                and fields
-                == "summary,description,status,assignee,reporter,labels,priority,created,updated,issuetype"
+                fields_param == ",".join(DEFAULT_READ_JIRA_FIELDS)
+                or fields_param == "*all"
             ):
                 # Default fields are being used - preserve the order
-                default_fields_list = fields.split(",")
+                default_fields_list = (
+                    fields_param.split(",")
+                    if fields_param != "*all"
+                    else list(DEFAULT_READ_JIRA_FIELDS)
+                )
                 additional_fields = []
 
                 # Add appropriate fields based on expand parameter
@@ -100,17 +107,12 @@ class IssuesMixin(
 
                 # Combine default fields with additional fields, preserving order
                 if additional_fields:
-                    fields = fields + "," + ",".join(additional_fields)
+                    fields_param = ",".join(default_fields_list + additional_fields)
             # Handle non-default fields string
 
             # Build expand parameter if provided
             expand_param = expand
 
-            # Convert fields to proper format if it's a list/tuple/set
-            fields_param = fields
-            if fields and isinstance(fields, list | tuple | set):
-                fields_param = ",".join(fields)
-
             # Convert properties to proper format if it's a list
             properties_param = properties
             if properties and isinstance(properties, list | tuple | set):
@@ -198,7 +200,7 @@ class IssuesMixin(
                 raise MCPAtlassianAuthenticationError(error_msg) from http_err
             else:
                 logger.error(f"HTTP error during API call: {http_err}", exc_info=False)
-                raise http_err
+                raise
         except Exception as e:
             error_msg = str(e)
             logger.error(f"Error retrieving issue {issue_key}: {error_msg}")
@@ -562,8 +564,8 @@ class IssuesMixin(
             elif "parent" in kwargs:
                 self._prepare_parent_fields(fields, kwargs)
 
-            # Add custom fields
-            self._add_custom_fields(fields, kwargs)
+            # Process **kwargs using the dynamic field map
+            self._process_additional_fields(fields, kwargs_copy)
 
             # Create the issue
             response = self.jira.create_issue(fields=fields)
@@ -670,56 +672,173 @@ class IssuesMixin(
             # Server/DC might use name instead of accountId
             fields["assignee"] = {"name": assignee}
 
-    def _add_custom_fields(
+    def _process_additional_fields(
         self, fields: dict[str, Any], kwargs: dict[str, Any]
     ) -> None:
         """
-        Add custom fields to issue.
+        Processes keyword arguments to add standard or custom fields to the issue fields dictionary.
+        Uses the dynamic field map from FieldsMixin to identify field IDs.
 
         Args:
             fields: The fields dictionary to update
-            kwargs: Additional fields from the user
+            kwargs: Additional fields provided via **kwargs
         """
-        field_ids = self.get_field_ids_to_epic()
+        # Ensure field map is loaded/cached
+        field_map = (
+            self._generate_field_map()
+        )  # Ensure map is ready (method from FieldsMixin)
+        if not field_map:
+            logger.error(
+                "Could not generate field map. Cannot process additional fields."
+            )
+            return
 
         # Process each kwarg
-        for key, value in kwargs.items():
-            # Handle fixVersions specifically as it's a standard field often required
-            if key.lower() == "fixversions":
-                fields["fixVersions"] = value
-                logger.debug("Added fixVersions from additional_fields: %s", value)
-                continue  # Handled fixVersions
-
-            # Explicitly handle components field
-            if key.lower() == "components":
-                # Assuming the value is already in the correct format e.g., [{'name': 'Vortex'}] or [{'id': '11004'}]
-                # We need the actual field ID for components. Let's try the common one, but this might need adjustment.
-                # If 'Components' field ID is known, use it directly. Otherwise, try a common default or log a warning.
-                component_field_id = field_ids.get(
-                    "Components", field_ids.get("components")
-                )  # Try both cases
-                if component_field_id:
-                    fields[component_field_id] = value
+        # Iterate over a copy to allow modification of the original kwargs if needed elsewhere
+        for key, value in kwargs.copy().items():
+            # Skip keys used internally for epic/parent handling or explicitly handled args like assignee/components
+            if key.startswith("__epic_") or key in ("parent", "assignee", "components"):
+                continue
+
+            normalized_key = key.lower()
+            api_field_id = None
+
+            # 1. Check if key is a known field name in the map
+            if normalized_key in field_map:
+                api_field_id = field_map[normalized_key]
+                logger.debug(
+                    f"Identified field '{key}' as '{api_field_id}' via name map."
+                )
+
+            # 2. Check if key is a direct custom field ID
+            elif key.startswith("customfield_"):
+                api_field_id = key
+                logger.debug(f"Identified field '{key}' as direct custom field ID.")
+
+            # 3. Check if key is a standard system field ID (like 'summary', 'priority')
+            elif key in field_map:  # Check original case for system fields
+                api_field_id = field_map[key]
+                logger.debug(f"Identified field '{key}' as standard system field ID.")
+
+            if api_field_id:
+                # Get the full field definition for formatting context if needed
+                field_definition = self.get_field_by_id(
+                    api_field_id
+                )  # From FieldsMixin
+                formatted_value = self._format_field_value_for_write(
+                    api_field_id, value, field_definition
+                )
+                if formatted_value is not None:  # Only add if formatting didn't fail
+                    fields[api_field_id] = formatted_value
                     logger.debug(
-                        f"Explicitly added components using field ID: {component_field_id}"
+                        f"Added field '{api_field_id}' from kwarg '{key}': {formatted_value}"
                     )
                 else:
-                    # Fallback or warning if component ID not found
                     logger.warning(
-                        "Could not find field ID for 'Components'. Components may not be set."
+                        f"Skipping field '{key}' due to formatting error or invalid value."
                     )
-                continue  # Skip further processing for components
+            else:
+                # 4. Unrecognized key - log a warning and skip
+                logger.warning(
+                    f"Ignoring unrecognized field '{key}' passed via kwargs."
+                )
 
-            if key in ("epic_name", "epic_link", "parent"):
-                continue  # Handled separately
+    def _format_field_value_for_write(
+        self, field_id: str, value: Any, field_definition: dict | None
+    ) -> Any:
+        """Formats field values for the Jira API."""
+        # Get schema type if definition is available
+        schema_type = (
+            field_definition.get("schema", {}).get("type") if field_definition else None
+        )
+        # Prefer name from definition if available, else use ID for logging/lookup
+        field_name_for_format = (
+            field_definition.get("name", field_id) if field_definition else field_id
+        )
 
-            # Check if this is a known field
-            # Use case-insensitive check for field names if needed, but rely on field_ids map primarily
-            if key in field_ids:
-                fields[field_ids[key]] = value
-            elif key.startswith("customfield_"):
-                # Direct custom field reference
-                fields[key] = value
+        # Example formatting rules based on standard field names (use lowercase for comparison)
+        normalized_name = field_name_for_format.lower()
+
+        if normalized_name == "priority":
+            if isinstance(value, str):
+                return {"name": value}
+            elif isinstance(value, dict) and ("name" in value or "id" in value):
+                return value  # Assume pre-formatted
+            else:
+                logger.warning(
+                    f"Invalid format for priority field: {value}. Expected string name or dict."
+                )
+                return None  # Or raise error
+        elif normalized_name == "labels":
+            if isinstance(value, list) and all(isinstance(item, str) for item in value):
+                return value
+            # Allow comma-separated string if passed via additional_fields JSON string
+            elif isinstance(value, str):
+                return [label.strip() for label in value.split(",") if label.strip()]
+            else:
+                logger.warning(
+                    f"Invalid format for labels field: {value}. Expected list of strings or comma-separated string."
+                )
+                return None
+        elif normalized_name in ["fixversions", "versions", "components"]:
+            # These expect lists of objects, typically {"name": "..."} or {"id": "..."}
+            if isinstance(value, list):
+                formatted_list = []
+                for item in value:
+                    if isinstance(item, str):
+                        formatted_list.append({"name": item})  # Convert simple strings
+                    elif isinstance(item, dict) and ("name" in item or "id" in item):
+                        formatted_list.append(item)  # Keep pre-formatted dicts
+                    else:
+                        logger.warning(
+                            f"Invalid item format in {normalized_name} list: {item}"
+                        )
+                return formatted_list
+            else:
+                logger.warning(
+                    f"Invalid format for {normalized_name} field: {value}. Expected list."
+                )
+                return None
+        elif normalized_name == "reporter":
+            if isinstance(value, str):
+                try:
+                    reporter_identifier = self._get_account_id(value)
+                    if self.config.is_cloud:
+                        return {"accountId": reporter_identifier}
+                    else:
+                        return {"name": reporter_identifier}
+                except ValueError as e:
+                    logger.warning(f"Could not format reporter field: {str(e)}")
+                    return None
+            elif isinstance(value, dict) and ("name" in value or "accountId" in value):
+                return value  # Assume pre-formatted
+            else:
+                logger.warning(f"Invalid format for reporter field: {value}")
+                return None
+        # Add more formatting rules for other standard fields based on schema_type or field_id
+        elif normalized_name == "duedate":
+            if isinstance(value, str):  # Basic check, could add date validation
+                return value
+            else:
+                logger.warning(
+                    f"Invalid format for duedate field: {value}. Expected YYYY-MM-DD string."
+                )
+                return None
+        elif schema_type == "datetime" and isinstance(value, str):
+            # Example: Ensure datetime fields are in ISO format if needed by API
+            try:
+                dt = parse_date(value)  # Assuming parse_date handles various inputs
+                return (
+                    dt.isoformat() if dt else value
+                )  # Return ISO or original if parse fails
+            except Exception:
+                logger.warning(
+                    f"Could not parse datetime for field {field_id}: {value}"
+                )
+                return value  # Return original on error
+
+        # Default: return value as is if no specific formatting needed/identified
+        return value
 
     def _handle_create_issue_error(self, exception: Exception, issue_type: str) -> None:
         """
@@ -800,14 +919,10 @@ class IssuesMixin(
                     except ValueError as e:
                         logger.warning(f"Could not update assignee: {str(e)}")
                 else:
-                    # Process regular fields
-                    field_ids = self.get_field_ids_to_epic()
-                    if key in field_ids:
-                        update_fields[field_ids[key]] = value
-                    elif key.startswith("customfield_"):
-                        update_fields[key] = value
-                    else:
-                        update_fields[key] = value
+                    # Process regular fields using _process_additional_fields
+                    # Create a temporary dict with just this field
+                    field_kwargs = {key: value}
+                    self._process_additional_fields(update_fields, field_kwargs)
 
             # Update the issue fields
             if update_fields:
@@ -1173,7 +1288,7 @@ class IssuesMixin(
                             ]
 
                 # Add any remaining custom fields
-                self._add_custom_fields(fields, issue_data)
+                self._process_additional_fields(fields, issue_data)
 
                 if validate_only:
                     # For validation, just log the issue that would be created
diff --git a/src/mcp_atlassian/jira/protocols.py b/src/mcp_atlassian/jira/protocols.py
index eaa9227..a6b597c 100644
--- a/src/mcp_atlassian/jira/protocols.py
+++ b/src/mcp_atlassian/jira/protocols.py
@@ -121,6 +121,26 @@ class EpicOperationsProto(Protocol):
 class FieldsOperationsProto(Protocol):
     """Protocol defining fields operations interface."""
 
+    @abstractmethod
+    def _generate_field_map(self, force_regenerate: bool = False) -> dict[str, str]:
+        """
+        Generates and caches a map of lowercase field names to field IDs.
+
+        Args:
+            force_regenerate: If True, forces regeneration even if cache exists.
+
+        Returns:
+            A dictionary mapping lowercase field names and field IDs to actual field IDs.
+        """
+
+    @abstractmethod
+    def get_field_by_id(
+        self, field_id: str, refresh: bool = False
+    ) -> dict[str, Any] | None:
+        """
+        Get field definition by ID.
+        """
+
     @abstractmethod
     def get_field_ids_to_epic(self) -> dict[str, str]:
         """
diff --git a/src/mcp_atlassian/jira/search.py b/src/mcp_atlassian/jira/search.py
index 1a75104..1eb205e 100644
--- a/src/mcp_atlassian/jira/search.py
+++ b/src/mcp_atlassian/jira/search.py
@@ -8,6 +8,7 @@ from requests.exceptions import HTTPError
 from ..exceptions import MCPAtlassianAuthenticationError
 from ..models.jira import JiraSearchResult
 from .client import JiraClient
+from .constants import DEFAULT_READ_JIRA_FIELDS
 from .protocols import IssueOperationsProto
 
 logger = logging.getLogger("mcp-jira")
@@ -19,10 +20,7 @@ class SearchMixin(JiraClient, IssueOperationsProto):
     def search_issues(
         self,
         jql: str,
-        fields: list[str]
-        | tuple[str, ...]
-        | set[str]
-        | str = "summary,description,status,assignee,reporter,labels,priority,created,updated,issuetype",
+        fields: list[str] | tuple[str, ...] | set[str] | str | None = None,
         start: int = 0,
         limit: int = 50,
         expand: str | None = None,
@@ -76,7 +74,10 @@ class SearchMixin(JiraClient, IssueOperationsProto):
                 logger.info(f"Applied projects filter to query: {jql}")
 
             # Convert fields to proper format if it's a list/tuple/set
-            if isinstance(fields, list | tuple | set):
+            fields_param: str | None
+            if fields is None:  # Use default if None
+                fields_param = ",".join(DEFAULT_READ_JIRA_FIELDS)
+            elif isinstance(fields, list | tuple | set):
                 fields_param = ",".join(fields)
             else:
                 fields_param = fields
@@ -97,7 +98,7 @@ class SearchMixin(JiraClient, IssueOperationsProto):
 
             # Convert the response to a search result model
             search_result = JiraSearchResult.from_api_response(
-                response, base_url=self.config.url, requested_fields=fields
+                response, base_url=self.config.url, requested_fields=fields_param
             )
 
             # Return the full search result object
@@ -124,7 +125,7 @@ class SearchMixin(JiraClient, IssueOperationsProto):
         self,
         board_id: str,
         jql: str,
-        fields: str = "*all",
+        fields: str | None = None,
         start: int = 0,
         limit: int = 50,
         expand: str | None = None,
@@ -147,10 +148,15 @@ class SearchMixin(JiraClient, IssueOperationsProto):
             Exception: If there is an error getting board issues
         """
         try:
+            # Determine fields_param
+            fields_param = fields
+            if fields_param is None:
+                fields_param = ",".join(DEFAULT_READ_JIRA_FIELDS)
+
             response = self.jira.get_issues_for_board(
                 board_id=board_id,
                 jql=jql,
-                fields=fields,
+                fields=fields_param,
                 start=start,
                 limit=limit,
                 expand=expand,
@@ -162,7 +168,7 @@ class SearchMixin(JiraClient, IssueOperationsProto):
 
             # Convert the response to a search result model
             search_result = JiraSearchResult.from_api_response(
-                response, base_url=self.config.url, requested_fields=fields
+                response, base_url=self.config.url, requested_fields=fields_param
             )
             return search_result
         except requests.HTTPError as e:
@@ -181,7 +187,7 @@ class SearchMixin(JiraClient, IssueOperationsProto):
     def get_sprint_issues(
         self,
         sprint_id: str,
-        fields: str = "*all",
+        fields: str | None = None,
         start: int = 0,
         limit: int = 50,
     ) -> JiraSearchResult:
@@ -201,6 +207,11 @@ class SearchMixin(JiraClient, IssueOperationsProto):
             Exception: If there is an error getting board issues
         """
         try:
+            # Determine fields_param
+            fields_param = fields
+            if fields_param is None:
+                fields_param = ",".join(DEFAULT_READ_JIRA_FIELDS)
+
             response = self.jira.get_sprint_issues(
                 sprint_id=sprint_id,
                 start=start,
@@ -213,7 +224,7 @@ class SearchMixin(JiraClient, IssueOperationsProto):
 
             # Convert the response to a search result model
             search_result = JiraSearchResult.from_api_response(
-                response, base_url=self.config.url, requested_fields=fields
+                response, base_url=self.config.url, requested_fields=fields_param
             )
             return search_result
         except requests.HTTPError as e:
diff --git a/src/mcp_atlassian/server.py b/src/mcp_atlassian/server.py
index 6b46250..b4c4d35 100644
--- a/src/mcp_atlassian/server.py
+++ b/src/mcp_atlassian/server.py
@@ -13,6 +13,7 @@ from .confluence import ConfluenceFetcher
 from .confluence.config import ConfluenceConfig
 from .jira import JiraFetcher
 from .jira.config import JiraConfig
+from .jira.constants import DEFAULT_READ_JIRA_FIELDS
 from .utils.io import is_read_only_mode
 from .utils.logging import log_config_param
 from .utils.tools import get_enabled_tools, should_include_tool
@@ -489,7 +490,7 @@ async def list_tools() -> list[Tool]:
                         "fields": {
                             "type": "string",
                             "description": "Fields to return. Can be a comma-separated list (e.g., 'summary,status,customfield_10010'), '*all' for all fields (including custom fields), or omitted for essential fields only",
-                            "default": "summary,description,status,assignee,reporter,labels,priority,created,updated,issuetype",
+                            "default": ",".join(DEFAULT_READ_JIRA_FIELDS),
                         },
                         "expand": {
                             "type": "string",
@@ -548,7 +549,7 @@ async def list_tools() -> list[Tool]:
                                 "Use '*all' for all fields, or specify individual "
                                 "fields like 'summary,status,assignee,priority'"
                             ),
-                            "default": "summary,description,status,assignee,reporter,labels,priority,created,updated,issuetype",
+                            "default": ",".join(DEFAULT_READ_JIRA_FIELDS),
                         },
                         "limit": {
                             "type": "number",
@@ -758,7 +759,7 @@ async def list_tools() -> list[Tool]:
                                 "Use '*all' for all fields, or specify individual "
                                 "fields like 'summary,status,assignee,priority'"
                             ),
-                            "default": "*all",
+                            "default": ",".join(DEFAULT_READ_JIRA_FIELDS),
                         },
                         "startAt": {
                             "type": "number",
@@ -862,7 +863,7 @@ async def list_tools() -> list[Tool]:
                                 "Use '*all' for all fields, or specify individual "
                                 "fields like 'summary,status,assignee,priority'"
                             ),
-                            "default": "*all",
+                            "default": ",".join(DEFAULT_READ_JIRA_FIELDS),
                         },
                         "startAt": {
                             "type": "number",
@@ -1647,18 +1648,17 @@ async def call_tool(name: str, arguments: Any) -> Sequence[TextContent]:
             if not ctx or not ctx.jira:
                 raise ValueError("Jira is not configured.")
 
+            # Extract arguments
             issue_key = arguments.get("issue_key")
-            fields = arguments.get(
-                "fields",
-                "summary,description,status,assignee,reporter,labels,priority,created,updated,issuetype",
-            )
+            fields = arguments.get("fields", ",".join(DEFAULT_READ_JIRA_FIELDS))
             expand = arguments.get("expand")
             comment_limit = arguments.get("comment_limit", 10)
             properties = arguments.get("properties")
             update_history = arguments.get("update_history", True)
 
+            # Get the issue
             issue = ctx.jira.get_issue(
-                issue_key,
+                issue_key=issue_key,
                 fields=fields,
                 expand=expand,
                 comment_limit=comment_limit,
@@ -1666,7 +1666,8 @@ async def call_tool(name: str, arguments: Any) -> Sequence[TextContent]:
                 update_history=update_history,
             )
 
-            result = {"content": issue.to_simplified_dict()}
+            # Convert the issue data to a simplified dict
+            result = issue.to_simplified_dict()
 
             return [
                 TextContent(
@@ -1678,38 +1679,30 @@ async def call_tool(name: str, arguments: Any) -> Sequence[TextContent]:
             if not ctx or not ctx.jira:
                 raise ValueError("Jira is not configured.")
 
+            # Extract arguments
             jql = arguments.get("jql")
-            fields = arguments.get(
-                "fields",
-                "summary,description,status,assignee,reporter,labels,priority,created,updated,issuetype",
-            )
+            fields = arguments.get("fields", ",".join(DEFAULT_READ_JIRA_FIELDS))
             limit = min(int(arguments.get("limit", 10)), 50)
             projects_filter = arguments.get("projects_filter")
-            start_at = int(arguments.get("startAt", 0))  # Get startAt
+            start_at = arguments.get("startAt", 0)
+            expand = arguments.get("expand")
 
+            # Perform the search
             search_result = ctx.jira.search_issues(
-                jql,
+                jql=jql,
                 fields=fields,
                 limit=limit,
-                start=start_at,  # Pass start_at here
+                start=start_at,
+                expand=expand,
                 projects_filter=projects_filter,
             )
 
-            # Format results using the to_simplified_dict method
-            issues = [issue.to_simplified_dict() for issue in search_result.issues]
-
-            # Include metadata in the response
-            response = {
-                "total": search_result.total,
-                "start_at": search_result.start_at,
-                "max_results": search_result.max_results,
-                "issues": issues,
-            }
+            # Convert to simplified dict and return
+            result = search_result.to_simplified_dict()
 
             return [
                 TextContent(
-                    type="text",
-                    text=json.dumps(response, indent=2, ensure_ascii=False),
+                    type="text", text=json.dumps(result, indent=2, ensure_ascii=False)
                 )
             ]
 
@@ -1892,14 +1885,15 @@ async def call_tool(name: str, arguments: Any) -> Sequence[TextContent]:
             if not ctx or not ctx.jira:
                 raise ValueError("Jira is not configured.")
 
+            # Extract arguments
             board_id = arguments.get("board_id")
             jql = arguments.get("jql")
-            fields = arguments.get("fields", "*all")
-
-            start_at = int(arguments.get("startAt", 0))
+            fields = arguments.get("fields", ",".join(DEFAULT_READ_JIRA_FIELDS))
+            start_at = arguments.get("startAt", 0)
             limit = min(int(arguments.get("limit", 10)), 50)
-            expand = arguments.get("expand", "version")
+            expand = arguments.get("expand")
 
+            # Get board issues
             search_result = ctx.jira.get_board_issues(
                 board_id=board_id,
                 jql=jql,
@@ -1909,21 +1903,12 @@ async def call_tool(name: str, arguments: Any) -> Sequence[TextContent]:
                 expand=expand,
             )
 
-            # Format results
-            issues = [issue.to_simplified_dict() for issue in search_result.issues]
-
-            # Include metadata in the response
-            response = {
-                "total": search_result.total,
-                "start_at": search_result.start_at,
-                "max_results": search_result.max_results,
-                "issues": issues,
-            }
+            # Convert to simplified dict
+            result = search_result.to_simplified_dict()
 
             return [
                 TextContent(
-                    type="text",
-                    text=json.dumps(response, indent=2, ensure_ascii=False),
+                    type="text", text=json.dumps(result, indent=2, ensure_ascii=False)
                 )
             ]
 
@@ -1982,11 +1967,13 @@ async def call_tool(name: str, arguments: Any) -> Sequence[TextContent]:
             if not ctx or not ctx.jira:
                 raise ValueError("Jira is not configured.")
 
+            # Extract arguments
             sprint_id = arguments.get("sprint_id")
-            fields = arguments.get("fields", "*all")
-            start_at = int(arguments.get("startAt", 0))
+            fields = arguments.get("fields", ",".join(DEFAULT_READ_JIRA_FIELDS))
+            start_at = arguments.get("startAt", 0)
             limit = min(int(arguments.get("limit", 10)), 50)
 
+            # Get sprint issues
             search_result = ctx.jira.get_sprint_issues(
                 sprint_id=sprint_id,
                 fields=fields,
@@ -1994,21 +1981,12 @@ async def call_tool(name: str, arguments: Any) -> Sequence[TextContent]:
                 limit=limit,
             )
 
-            # Format results
-            issues = [issue.to_simplified_dict() for issue in search_result.issues]
-
-            # Include metadata in the response
-            response = {
-                "total": search_result.total,
-                "start_at": search_result.start_at,
-                "max_results": search_result.max_results,
-                "issues": issues,
-            }
+            # Convert to simplified dict
+            result = search_result.to_simplified_dict()
 
             return [
                 TextContent(
-                    type="text",
-                    text=json.dumps(response, indent=2, ensure_ascii=False),
+                    type="text", text=json.dumps(result, indent=2, ensure_ascii=False)
                 )
             ]
 
@@ -2090,6 +2068,9 @@ async def call_tool(name: str, arguments: Any) -> Sequence[TextContent]:
                     additional_fields = json.loads(arguments.get("additional_fields"))
                 except json.JSONDecodeError:
                     raise ValueError("Invalid JSON in additional_fields")
+                except TypeError:
+                    # Handle case where additional_fields might be None
+                    pass
 
             # Create the issue
             issue = ctx.jira.create_issue(


Test patch:
diff --git a/tests/unit/jira/conftest.py b/tests/unit/jira/conftest.py
index 5d2465a..f32e27b 100644
--- a/tests/unit/jira/conftest.py
+++ b/tests/unit/jira/conftest.py
@@ -40,6 +40,49 @@ def mock_atlassian_jira():
     """Mock the Atlassian Jira client."""
     mock_jira = MagicMock()
 
+    mock_jira.get_all_fields.return_value = [
+        {"id": "summary", "name": "Summary", "schema": {"type": "string"}},
+        {"id": "description", "name": "Description", "schema": {"type": "string"}},
+        {"id": "issuetype", "name": "Issue Type", "schema": {"type": "issuetype"}},
+        {"id": "status", "name": "Status", "schema": {"type": "status"}},
+        {"id": "priority", "name": "Priority", "schema": {"type": "priority"}},
+        {
+            "id": "labels",
+            "name": "Labels",
+            "schema": {"type": "array", "items": "string"},
+        },
+        {"id": "assignee", "name": "Assignee", "schema": {"type": "user"}},
+        {"id": "reporter", "name": "Reporter", "schema": {"type": "user"}},
+        {"id": "created", "name": "Created", "schema": {"type": "datetime"}},
+        {"id": "updated", "name": "Updated", "schema": {"type": "datetime"}},
+        {
+            "id": "fixVersions",
+            "name": "Fix Version/s",
+            "schema": {"type": "array", "items": "version"},
+        },
+        {
+            "id": "customfield_10010",
+            "name": "Epic Link",
+            "schema": {
+                "type": "string",
+                "custom": "com.pyxis.greenhopper.jira:gh-epic-link",
+            },
+        },
+        {
+            "id": "customfield_10011",
+            "name": "Epic Name",
+            "schema": {
+                "type": "string",
+                "custom": "com.pyxis.greenhopper.jira:gh-epic-label",
+            },
+        },
+        {
+            "id": "customfield_10012",
+            "name": "Story Points",
+            "schema": {"type": "number"},
+        },
+    ]
+
     # Set up common method returns
     mock_jira.myself.return_value = {"accountId": "test-account-id"}
     mock_jira.get_issue.return_value = {
diff --git a/tests/unit/jira/test_fields.py b/tests/unit/jira/test_fields.py
index 89e84fc..859c863 100644
--- a/tests/unit/jira/test_fields.py
+++ b/tests/unit/jira/test_fields.py
@@ -131,13 +131,17 @@ class TestFieldsMixin:
         # Verify the result
         assert result == "summary"
 
-    def test_get_field_id_partial_match(self, fields_mixin: FieldsMixin, mock_fields):
-        """Test get_field_id finds field by partial match."""
+    def test_get_field_id_exact_match_case_insensitive(
+        self, fields_mixin: FieldsMixin, mock_fields
+    ):
+        """Test get_field_id finds field by exact match (case-insensitive) using the map."""
         # Set up the fields
         fields_mixin.get_fields = MagicMock(return_value=mock_fields)
+        # Ensure the map is generated based on the mock fields for this test
+        fields_mixin._generate_field_map(force_regenerate=True)
 
-        # Call the method with partial name
-        result = fields_mixin.get_field_id("Epic")
+        # Call the method with exact name (case-insensitive)
+        result = fields_mixin.get_field_id("epic link")
 
         # Verify the result (should find Epic Link as first match)
         assert result == "customfield_10010"
@@ -319,23 +323,19 @@ class TestFieldsMixin:
         """Test get_field_ids_to_epic extracts field IDs from field definitions."""
         # Set up the fields
         fields_mixin.get_fields = MagicMock(return_value=mock_fields)
+        # Ensure field map is generated
+        fields_mixin._generate_field_map(force_regenerate=True)
 
         # Call the method
         result = fields_mixin.get_field_ids_to_epic()
-        expected_result = {
-            "Assignee": "assignee",
-            "Description": "description",
-            "Epic Link": "customfield_10010",
-            "Epic Name": "customfield_10011",
-            "Status": "status",
-            "Story Points": "customfield_10012",
-            "Summary": "summary",
-            "epic_link": "customfield_10010",
-            "epic_name": "customfield_10011",
-        }
 
-        # Verify the result
-        assert result == expected_result
+        # Verify that epic-specific fields are properly identified
+        assert "epic_link" in result
+        assert "Epic Link" in result
+        assert result["epic_link"] == "customfield_10010"
+        assert "epic_name" in result
+        assert "Epic Name" in result
+        assert result["epic_name"] == "customfield_10011"
 
     def test_get_jira_field_ids_error(self, fields_mixin: FieldsMixin):
         """Test get_field_ids_to_epic handles errors gracefully."""
diff --git a/tests/unit/jira/test_issues.py b/tests/unit/jira/test_issues.py
index 7daeb9e..92fc5f7 100644
--- a/tests/unit/jira/test_issues.py
+++ b/tests/unit/jira/test_issues.py
@@ -1,6 +1,6 @@
 """Tests for the Jira Issues mixin."""
 
-from unittest.mock import MagicMock, patch
+from unittest.mock import ANY, MagicMock, patch
 
 import pytest
 
@@ -29,19 +29,20 @@ class TestIssuesMixin:
         return mixin
 
     def test_get_issue_basic(self, issues_mixin: IssuesMixin):
-        """Test basic functionality of get_issue."""
-        # Setup mock
-        mock_issue = {
-            "id": "12345",
+        """Test retrieving an issue by key."""
+        # Mock the API response
+        issues_mixin.jira.get_issue.return_value = {
+            "id": "10001",
             "key": "TEST-123",
             "fields": {
                 "summary": "Test Issue",
-                "description": "Test Issue\n\nThis is a test issue",
+                "description": "This is a test issue",
                 "status": {"name": "Open"},
                 "issuetype": {"name": "Bug"},
+                "created": "2023-01-01T00:00:00.000+0000",
+                "updated": "2023-01-02T00:00:00.000+0000",
             },
         }
-        issues_mixin.jira.get_issue.return_value = mock_issue
 
         # Call the method
         result = issues_mixin.get_issue("TEST-123")
@@ -50,7 +51,7 @@ class TestIssuesMixin:
         issues_mixin.jira.get_issue.assert_called_once_with(
             "TEST-123",
             expand=None,
-            fields="summary,description,status,assignee,reporter,labels,priority,created,updated,issuetype",
+            fields=ANY,
             properties=None,
             update_history=True,
         )
@@ -59,7 +60,7 @@ class TestIssuesMixin:
         assert isinstance(result, JiraIssue)
         assert result.key == "TEST-123"
         assert result.summary == "Test Issue"
-        assert result.description == "Test Issue\n\nThis is a test issue"
+        assert result.description == "This is a test issue"
 
         # Check Jira fields mapping
         assert result.status is not None
@@ -122,68 +123,56 @@ class TestIssuesMixin:
         assert issue.comments[0].body == "This is a comment"
 
     def test_get_issue_with_epic_info(self, issues_mixin: IssuesMixin):
-        """Test getting an issue with epic information."""
-        # Mock the issue data
-        issue_data = {
-            "id": "12345",
-            "key": "TEST-123",
-            "fields": {
-                "summary": "Test Issue",
-                "description": "Test Description",
-                "status": {"name": "Open"},
-                "issuetype": {"name": "Story"},
-                "created": "2023-01-01T00:00:00.000+0000",
-                "updated": "2023-01-02T00:00:00.000+0000",
-                "customfield_10014": "EPIC-456",  # Epic Link field
-                "schema": {
+        """Test retrieving issue with epic information."""
+        try:
+            # Mock the API responses for get_issue
+            issues_mixin.jira.get_issue.side_effect = [
+                # First call - the issue
+                {
+                    "id": "10001",
+                    "key": "TEST-123",
                     "fields": {
-                        "customfield_10014": {"name": "Epic Link", "type": "string"},
-                        "customfield_10011": {"name": "Epic Name", "type": "string"},
-                    }
+                        "summary": "Test Issue",
+                        "description": "This is a test issue",
+                        "status": {"name": "Open"},
+                        "issuetype": {"name": "Story"},
+                        "customfield_10010": "EPIC-456",  # Epic Link field
+                        "created": "2023-01-01T00:00:00.000+0000",
+                        "updated": "2023-01-02T00:00:00.000+0000",
+                    },
                 },
-            },
-        }
-
-        # Mock the epic data
-        epic_data = {
-            "id": "67890",
-            "key": "EPIC-456",
-            "fields": {
-                "summary": "Test Epic",
-                "customfield_10011": "Epic Name",  # Epic Name field
-                "status": {"name": "In Progress"},
-                "issuetype": {"name": "Epic"},
-                "schema": {
+                # Second call - the epic
+                {
+                    "id": "10002",
+                    "key": "EPIC-456",
                     "fields": {
-                        "customfield_10011": {"name": "Epic Name", "type": "string"}
-                    }
+                        "summary": "Epic Issue",
+                        "description": "This is an epic",
+                        "status": {"name": "In Progress"},
+                        "issuetype": {"name": "Epic"},
+                        "customfield_10011": "Epic Name Value",  # Epic Name field
+                        "created": "2023-01-01T00:00:00.000+0000",
+                        "updated": "2023-01-02T00:00:00.000+0000",
+                    },
                 },
-            },
-        }
-
-        # Mock the comments data
-        comments_data = {"comments": []}
+            ]
 
-        # Get field IDs for epics
-        mock_field_ids = {
-            "epic_link": "customfield_10014",
-            "epic_name": "customfield_10011",
-        }
-        with patch.object(
-            issues_mixin, "get_field_ids_to_epic", return_value=mock_field_ids
-        ):
-            # Setup the mocked responses
-            issues_mixin.jira.get_issue.side_effect = [issue_data, epic_data]
-            issues_mixin.jira.issue_get_comments.return_value = comments_data
+            # Mock get_field_ids_to_epic
+            issues_mixin.get_field_ids_to_epic = MagicMock(
+                return_value={
+                    "epic_link": "customfield_10010",
+                    "epic_name": "customfield_10011",
+                }
+            )
 
-            # Call the method
+            # Call the method - just use get_issue without the include_epic_info parameter
             issue = issues_mixin.get_issue("TEST-123")
 
             # Verify the API calls
             issues_mixin.jira.get_issue.assert_any_call(
                 "TEST-123",
                 expand=None,
-                fields="summary,description,status,assignee,reporter,labels,priority,created,updated,issuetype",
+                fields=ANY,
                 properties=None,
                 update_history=True,
             )
@@ -196,22 +185,24 @@ class TestIssuesMixin:
             )
 
             # Verify the issue
-            assert issue.id == "12345"
             assert issue.key == "TEST-123"
             assert issue.summary == "Test Issue"
 
-            # Verify the epic info was added
-            assert issue.epic_key == "EPIC-456"
-            assert issue.epic_name == "Epic Name"
+            # Verify that the epic information is in the custom fields
+            assert issue.custom_fields.get("customfield_10010") == "EPIC-456"
+            assert issue.custom_fields.get("customfield_10011") == "Epic Name Value"
+
+        except Exception as e:
+            pytest.fail(f"Test failed: {e}")
 
     def test_get_issue_error_handling(self, issues_mixin: IssuesMixin):
-        """Test error handling when getting an issue."""
-        # Make the API call raise an exception
+        """Test error handling in get_issue."""
+        # Mock the API to raise an exception
         issues_mixin.jira.get_issue.side_effect = Exception("API error")
 
         # Call the method and verify it raises the expected exception
         with pytest.raises(
-            Exception, match="Error retrieving issue TEST-123: API error"
+            Exception, match=r"Error retrieving issue TEST-123: API error"
         ):
             issues_mixin.get_issue("TEST-123")
 
@@ -717,14 +708,16 @@ class TestIssuesMixin:
         ):
             issues_mixin.delete_issue("TEST-123")
 
-    def test_add_custom_fields_with_fixversions(self, issues_mixin: IssuesMixin):
-        """Test _add_custom_fields properly handles fixVersions field."""
+    def test_process_additional_fields_with_fixversions(
+        self, issues_mixin: IssuesMixin
+    ):
+        """Test _process_additional_fields properly handles fixVersions field."""
         # Initialize test data
         fields = {}
         kwargs = {"fixVersions": [{"name": "TestRelease"}]}
 
         # Call the method
-        issues_mixin._add_custom_fields(fields, kwargs)
+        issues_mixin._process_additional_fields(fields, kwargs)
 
         # Verify fixVersions was added correctly to fields
         assert "fixVersions" in fields
@@ -934,7 +927,7 @@ class TestIssuesMixin:
         issues_mixin.jira.get_issue.assert_called_with(
             "TEST-123",
             expand=None,
-            fields="summary,description,status,assignee,reporter,labels,priority,created,updated,issuetype,properties",
+            fields=ANY,
             properties="property1,property2",
             update_history=True,
         )
@@ -947,7 +940,7 @@ class TestIssuesMixin:
         issues_mixin.jira.get_issue.assert_called_with(
             "TEST-123",
             expand=None,
-            fields="summary,description,status,assignee,reporter,labels,priority,created,updated,issuetype,properties",
+            fields=ANY,
             properties="property1,property2",
             update_history=True,
         )
@@ -968,7 +961,7 @@ class TestIssuesMixin:
         issues_mixin.jira.get_issue.assert_called_with(
             "TEST-123",
             expand=None,
-            fields="summary,description,status,assignee,reporter,labels,priority,created,updated,issuetype",
+            fields=ANY,
             properties=None,
             update_history=False,
         )
@@ -1440,3 +1433,90 @@ class TestIssuesMixin:
                 "issueIdsOrKeys": ["TEST-1", "TEST-2"],
             },
         )
+
+    def test_create_issue_with_labels(self, issues_mixin: IssuesMixin):
+        """Test creating an issue with labels in additional_fields."""
+        # Mock create_issue response
+        create_response = {"id": "12345", "key": "TEST-123"}
+        issues_mixin.jira.create_issue.return_value = create_response
+
+        # Mock the issue data for get_issue
+        issue_data = {
+            "id": "12345",
+            "key": "TEST-123",
+            "fields": {
+                "summary": "Test Issue",
+                "description": "This is a test issue",
+                "status": {"name": "Open"},
+                "issuetype": {"name": "Bug"},
+                "labels": ["bug", "frontend"],
+            },
+        }
+        issues_mixin.jira.get_issue.return_value = issue_data
+
+        # Create the issue with labels as a list
+        result = issues_mixin.create_issue(
+            project_key="TEST",
+            summary="Test Issue",
+            issue_type="Bug",
+            description="This is a test issue",
+            labels=["bug", "frontend"],
+        )
+
+        # Verify the API call
+        issues_mixin.jira.create_issue.assert_called_once()
+        call_kwargs = issues_mixin.jira.create_issue.call_args[1]
+        assert "fields" in call_kwargs
+        fields = call_kwargs["fields"]
+
+        # Verify labels were added to the fields
+        assert "labels" in fields
+        assert fields["labels"] == ["bug", "frontend"]
+
+        # Verify result
+        assert result.key == "TEST-123"
+        assert result.labels == ["bug", "frontend"]
+
+    def test_create_issue_with_labels_as_string(self, issues_mixin: IssuesMixin):
+        """Test creating an issue with labels as comma-separated string in additional_fields."""
+        # Mock create_issue response
+        create_response = {"id": "12345", "key": "TEST-123"}
+        issues_mixin.jira.create_issue.return_value = create_response
+
+        # Mock the issue data for get_issue
+        issue_data = {
+            "id": "12345",
+            "key": "TEST-123",
+            "fields": {
+                "summary": "Test Issue",
+                "description": "This is a test issue",
+                "status": {"name": "Open"},
+                "issuetype": {"name": "Bug"},
+                "labels": ["bug", "frontend"],
+            },
+        }
+        issues_mixin.jira.get_issue.return_value = issue_data
+
+        # Create the issue with labels as a comma-separated string
+        # Pass labels directly instead of through additional_fields
+        result = issues_mixin.create_issue(
+            project_key="TEST",
+            summary="Test Issue",
+            issue_type="Bug",
+            description="This is a test issue",
+            labels="bug,frontend",  # Pass as string and let _format_field_value_for_write handle it
+        )
+
+        # Verify the API call
+        issues_mixin.jira.create_issue.assert_called_once()
+        call_kwargs = issues_mixin.jira.create_issue.call_args[1]
+        assert "fields" in call_kwargs
+        fields = call_kwargs["fields"]
+
+        # Verify labels were parsed and added to the fields
+        assert "labels" in fields
+        assert fields["labels"] == ["bug", "frontend"]
+
+        # Verify result
+        assert result.key == "TEST-123"
+        assert result.labels == ["bug", "frontend"]
diff --git a/tests/unit/jira/test_search.py b/tests/unit/jira/test_search.py
index e9de8ae..dff3310 100644
--- a/tests/unit/jira/test_search.py
+++ b/tests/unit/jira/test_search.py
@@ -96,7 +96,6 @@ class TestSearchMixin:
 
         # Define expected kwargs based on whether it's Cloud or Server
         expected_kwargs = {
-            "fields": "summary,description,status,assignee,reporter,labels,priority,created,updated,issuetype",
             "limit": 10,
             "expand": None,
         }
@@ -105,7 +104,9 @@ class TestSearchMixin:
         if not is_cloud:
             expected_kwargs["start"] = 0
 
-        expected_method_mock.assert_called_once_with(jql_query, **expected_kwargs)
+        expected_method_mock.assert_called_once_with(
+            jql_query, fields=ANY, **expected_kwargs
+        )
 
         # Assert: Other method was not called
         other_method_mock = getattr(search_mixin.jira, other_method_name)
@@ -142,7 +143,7 @@ class TestSearchMixin:
         # Verify
         search_mixin.jira.jql.assert_called_once_with(
             "project = TEST",
-            fields="summary,description,status,assignee,reporter,labels,priority,created,updated,issuetype",
+            fields=ANY,
             start=0,
             limit=50,
             expand=None,
@@ -287,7 +288,7 @@ class TestSearchMixin:
         result = search_mixin.search_issues("text ~ 'test'", projects_filter="TEST")
         search_mixin.jira.jql.assert_called_with(
             "(text ~ 'test') AND project = TEST",
-            fields="summary,description,status,assignee,reporter,labels,priority,created,updated,issuetype",
+            fields=ANY,
             start=0,
             limit=50,
             expand=None,
@@ -299,7 +300,7 @@ class TestSearchMixin:
         result = search_mixin.search_issues("text ~ 'test'", projects_filter="TEST,DEV")
         search_mixin.jira.jql.assert_called_with(
             '(text ~ \'test\') AND project IN ("TEST", "DEV")',
-            fields="summary,description,status,assignee,reporter,labels,priority,created,updated,issuetype",
+            fields=ANY,
             start=0,
             limit=50,
             expand=None,
@@ -308,7 +309,7 @@ class TestSearchMixin:
         assert result.total == 1
 
     def test_search_issues_with_config_projects_filter(self, search_mixin: SearchMixin):
-        """Test search using projects filter from config."""
+        """Test search with projects filter from config."""
         # Setup mock response
         mock_issues = {
             "issues": [
@@ -334,7 +335,7 @@ class TestSearchMixin:
         result = search_mixin.search_issues("text ~ 'test'")
         search_mixin.jira.jql.assert_called_with(
             '(text ~ \'test\') AND project IN ("TEST", "DEV")',
-            fields="summary,description,status,assignee,reporter,labels,priority,created,updated,issuetype",
+            fields=ANY,
             start=0,
             limit=50,
             expand=None,
@@ -346,7 +347,7 @@ class TestSearchMixin:
         result = search_mixin.search_issues("text ~ 'test'", projects_filter="OVERRIDE")
         search_mixin.jira.jql.assert_called_with(
             "(text ~ 'test') AND project = OVERRIDE",
-            fields="summary,description,status,assignee,reporter,labels,priority,created,updated,issuetype",
+            fields=ANY,
             start=0,
             limit=50,
             expand=None,
@@ -360,7 +361,7 @@ class TestSearchMixin:
         )
         search_mixin.jira.jql.assert_called_with(
             '(text ~ \'test\') AND project IN ("OVER1", "OVER2")',
-            fields="summary,description,status,assignee,reporter,labels,priority,created,updated,issuetype",
+            fields=ANY,
             start=0,
             limit=50,
             expand=None,