Repository: sooperset/mcp-atlassian

Issue:
jira_search fields not working

I expected  fields of `jira search` arguments are Fields to return.  but it doesnt work.

- actual arguments
```json
{
  "jql": "project = PROJECT AND cf[10049] IS NOT EMPTY ORDER BY cf[10049] DESC",
  "fields": "summary,assignee,customfield_10049",
  "limit": 100
}

```

actual result has not customfield_10049. also result has other field. 

```
    "id": 
    "summary": 
    "key": 
    "description": null,
    "created": "",
    "updated": "",
    "status": null,
    "issue_type": null,
    "priority": null,
    "assignee": {
      "display_name": 
      "name": 
      "email": 
      "avatar_url": "
      ...
```

Discussion:
@sooperset: Hi @youngkyo0504 
Thank you for reporting this issue. I've implemented a fix #142  that addresses both problems you identified:

1. The `jira_search` fields parameter now properly filters which fields are returned in the response
2. Custom fields are now included when requested

This has been merged and will be part of the next release. Please let us know if you encounter any other issues.@youngkyo0504: Thank you. You're incredibly fast

Solution patch:
diff --git a/src/mcp_atlassian/jira/search.py b/src/mcp_atlassian/jira/search.py
index 877dfbc..7de8000 100644
--- a/src/mcp_atlassian/jira/search.py
+++ b/src/mcp_atlassian/jira/search.py
@@ -43,7 +43,7 @@ class SearchMixin(JiraClient):
 
             # Convert the response to a search result model
             search_result = JiraSearchResult.from_api_response(
-                response, base_url=self.config.url
+                response, base_url=self.config.url, requested_fields=fields
             )
 
             # Return the list of issues
diff --git a/src/mcp_atlassian/models/jira.py b/src/mcp_atlassian/models/jira.py
index c63f883..5af84bd 100644
--- a/src/mcp_atlassian/models/jira.py
+++ b/src/mcp_atlassian/models/jira.py
@@ -391,6 +391,8 @@ class JiraIssue(ApiModel, TimestampMixin):
     url: str | None = None
     epic_key: str | None = None
     epic_name: str | None = None
+    custom_fields: dict[str, Any] = Field(default_factory=dict)
+    requested_fields: list[str] | None = None
 
     @property
     def page_content(self) -> str | None:
@@ -492,6 +494,7 @@ class JiraIssue(ApiModel, TimestampMixin):
             data: The issue data from the Jira API
             **kwargs: Additional context parameters, including:
                 - base_url: Base URL for constructing the issue URL
+                - requested_fields: List of fields that were requested (if any)
 
         Returns:
             A JiraIssue instance
@@ -512,6 +515,15 @@ class JiraIssue(ApiModel, TimestampMixin):
             logger.debug(f"Fields is not a dict, using empty dict: {type(fields)}")
             fields = {}
 
+        # Store requested fields if provided
+        requested_fields = kwargs.get("requested_fields")
+
+        # Extract custom fields - any field beginning with "customfield_"
+        custom_fields = {}
+        for key, value in fields.items():
+            if key.startswith("customfield_"):
+                custom_fields[key] = value
+
         # Process status
         status = None
         status_data = fields.get("status")
@@ -628,30 +640,99 @@ class JiraIssue(ApiModel, TimestampMixin):
             url=url,
             epic_key=epic_key,
             epic_name=epic_name,
+            custom_fields=custom_fields,
+            requested_fields=requested_fields,
         )
 
     def to_simplified_dict(self) -> dict[str, Any]:
-        """Convert to a simplified dictionary representation."""
-        return {
+        """
+        Convert to a simplified dictionary representation.
+
+        If requested_fields is provided, only those fields will be included
+        in the result, plus id and key which are always included.
+        """
+        # Start with the minimal set of fields that should always be included
+        result: dict[str, Any] = {
             "id": self.id,
-            "summary": self.summary,
             "key": self.key,
-            "description": self.description,
-            "created": self.format_timestamp(self.created),
-            "updated": self.format_timestamp(self.updated),
-            "status": self.status.to_simplified_dict() if self.status else None,
-            "issue_type": self.issue_type.to_simplified_dict()
+        }
+
+        # If no specific fields were requested, include all standard fields
+        if not self.requested_fields or "*all" in self.requested_fields:
+            result.update(
+                {
+                    "summary": self.summary,
+                    "description": self.description,
+                    "created": self.format_timestamp(self.created),
+                    "updated": self.format_timestamp(self.updated),
+                    "status": self.status.to_simplified_dict() if self.status else None,
+                    "issue_type": self.issue_type.to_simplified_dict()
+                    if self.issue_type
+                    else None,
+                    "priority": self.priority.to_simplified_dict()
+                    if self.priority
+                    else None,
+                    "assignee": self.assignee.to_simplified_dict()
+                    if self.assignee
+                    else None,
+                    "reporter": self.reporter.to_simplified_dict()
+                    if self.reporter
+                    else None,
+                    "labels": self.labels,
+                    "components": self.components,
+                    "comments": [
+                        comment.to_simplified_dict() for comment in self.comments
+                    ],
+                    "url": self.url,
+                }
+            )
+            return result
+
+        # If specific fields were requested, only include those
+        field_mapping = {
+            "summary": lambda: self.summary,
+            "description": lambda: self.description,
+            "created": lambda: self.format_timestamp(self.created),
+            "updated": lambda: self.format_timestamp(self.updated),
+            "status": lambda: self.status.to_simplified_dict() if self.status else None,
+            "issuetype": lambda: self.issue_type.to_simplified_dict()
             if self.issue_type
             else None,
-            "priority": self.priority.to_simplified_dict() if self.priority else None,
-            "assignee": self.assignee.to_simplified_dict() if self.assignee else None,
-            "reporter": self.reporter.to_simplified_dict() if self.reporter else None,
-            "labels": self.labels,
-            "components": self.components,
-            "comments": [comment.to_simplified_dict() for comment in self.comments],
-            "url": self.url,
+            "issue_type": lambda: self.issue_type.to_simplified_dict()
+            if self.issue_type
+            else None,
+            "priority": lambda: self.priority.to_simplified_dict()
+            if self.priority
+            else None,
+            "assignee": lambda: self.assignee.to_simplified_dict()
+            if self.assignee
+            else None,
+            "reporter": lambda: self.reporter.to_simplified_dict()
+            if self.reporter
+            else None,
+            "labels": lambda: self.labels,
+            "components": lambda: self.components,
+            "comment": lambda: [
+                comment.to_simplified_dict() for comment in self.comments
+            ],
+            "comments": lambda: [
+                comment.to_simplified_dict() for comment in self.comments
+            ],
+            "url": lambda: self.url,
+            "epic_key": lambda: self.epic_key,
+            "epic_name": lambda: self.epic_name,
         }
 
+        for field in self.requested_fields:
+            # Handle standard fields
+            if field in field_mapping:
+                result[field] = field_mapping[field]()
+            # Handle custom fields
+            elif field.startswith("customfield_") and field in self.custom_fields:
+                result[field] = self.custom_fields[field]
+
+        return result
+
 
 class JiraProject(ApiModel):
     """
@@ -912,6 +993,7 @@ class JiraSearchResult(ApiModel):
             data: The search result data from the Jira API
             **kwargs: Additional context parameters, including:
                 - base_url: Base URL for constructing issue URLs
+                - requested_fields: List of fields that were requested (if "*all" or specific fields)
 
         Returns:
             A JiraSearchResult instance
@@ -919,6 +1001,14 @@ class JiraSearchResult(ApiModel):
         if not data:
             return cls()
 
+        # Check if specific fields were requested
+        requested_fields = kwargs.get("requested_fields")
+
+        # If requested_fields is a comma-separated string, split it
+        if isinstance(requested_fields, str):
+            requested_fields = [field.strip() for field in requested_fields.split(",")]
+            kwargs["requested_fields"] = requested_fields
+
         # Convert raw issues to JiraIssue models
         issues = [
             JiraIssue.from_api_response(issue, **kwargs)


Test patch:
diff --git a/tests/unit/jira/test_search.py b/tests/unit/jira/test_search.py
index 82baa85..55f0e3c 100644
--- a/tests/unit/jira/test_search.py
+++ b/tests/unit/jira/test_search.py
@@ -285,15 +285,85 @@ class TestSearchMixin:
             search_mixin.get_epic_issues("EPIC-1")
 
     def test_parse_date(self, search_mixin):
-        """Test the actual implementation of _parse_date."""
-        # Test ISO format
-        result = search_mixin._parse_date("2024-01-01T12:34:56.789+0000")
-        assert result == "2024-01-01", f"Expected '2024-01-01' but got '{result}'"
-
-        # Test invalid format
-        result = search_mixin._parse_date("invalid date")
-        assert result == "invalid date", f"Expected 'invalid date' but got '{result}'"
-
-        # Test None value
-        result = search_mixin._parse_date(None)
-        assert result == "", f"Expected empty string but got '{result}'"
+        """Test the _parse_date method."""
+        # Test with a valid ISO date
+        result = search_mixin._parse_date("2023-01-15T14:30:45.123+0000")
+        assert result == "2023-01-15"
+
+        # Test with an empty string
+        result = search_mixin._parse_date("")
+        assert result == ""
+
+    def test_search_issues_with_fields_parameter(self, search_mixin):
+        """Test search with specific fields parameter, including custom fields."""
+        # Setup mock response with a custom field
+        mock_issues = {
+            "issues": [
+                {
+                    "id": "10001",
+                    "key": "TEST-123",
+                    "fields": {
+                        "summary": "Test issue with custom field",
+                        "assignee": {
+                            "displayName": "Test User",
+                            "emailAddress": "test@example.com",
+                            "active": True,
+                        },
+                        "customfield_10049": "Custom value",
+                        "issuetype": {"name": "Bug"},
+                        "status": {"name": "Open"},
+                        "description": "Issue description",
+                        "created": "2024-01-01T10:00:00.000+0000",
+                        "updated": "2024-01-01T11:00:00.000+0000",
+                        "priority": {"name": "High"},
+                    },
+                }
+            ],
+            "total": 1,
+            "startAt": 0,
+            "maxResults": 50,
+        }
+        search_mixin.jira.jql.return_value = mock_issues
+        search_mixin.config.url = "https://example.atlassian.net"
+
+        # Call the method with specific fields
+        result = search_mixin.search_issues(
+            "project = TEST", fields="summary,assignee,customfield_10049"
+        )
+
+        # Verify the JQL call includes the fields parameter
+        search_mixin.jira.jql.assert_called_once_with(
+            "project = TEST",
+            fields="summary,assignee,customfield_10049",
+            start=0,
+            limit=50,
+            expand=None,
+        )
+
+        # Verify results
+        assert isinstance(result, list)
+        assert len(result) == 1
+        issue = result[0]
+
+        # Convert to simplified dict to check field filtering
+        simplified = issue.to_simplified_dict()
+
+        # These fields should be included (plus id and key which are always included)
+        assert "id" in simplified
+        assert "key" in simplified
+        assert "summary" in simplified
+        assert "assignee" in simplified
+        assert "customfield_10049" in simplified
+
+        # These fields should NOT be included
+        assert "description" not in simplified
+        assert "status" not in simplified
+        assert "issue_type" not in simplified
+        assert "priority" not in simplified
+        assert "created" not in simplified
+        assert "updated" not in simplified
+
+        # Verify the values of included fields
+        assert simplified["summary"] == "Test issue with custom field"
+        assert simplified["assignee"]["name"] == "Test User"
+        assert simplified["customfield_10049"] == "Custom value"