Repository: sooperset/mcp-atlassian

Issue:
[Bug]: jira_get_epic_issues
### 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

When I use this server I am getting the following error
'Tool execution error: EpicsMixin.get_epic_issues() got an unexpected keyword argument 'start''

I can see the mixin being called at line 1731 in server.py
```
            search_result = ctx.jira.get_epic_issues(
                epic_key, start=start_at, limit=limit
            )
```
However the signature for this method in epics.py is
```
    def get_epic_issues(self, epic_key: str, limit: int = 50) -> list[JiraIssue]:
```



### Steps to Reproduce

Send a 'jira_get_epic_issues' command with an issue key.

### Expected Behavior

It looks like the API does accept a startAt parameter so the mixin should accept this and pass it on.

### Actual Behavior

```shell
'Tool execution error: EpicsMixin.get_epic_issues() got an unexpected keyword argument 'start''
```

### mcp-atlassian Version

0.6.5

### Installation Method

From PyPI (pip install mcp-atlassian / uv add mcp-atlassian)

### Operating System

Windows

### Python Version

3.12

### Atlassian Instance Type

Jira Cloud

### Client Application

vscode

### Additional Context

_No response_

Discussion:
@sooperset: Thanks @fergoid, great catch! You're right, the `start` parameter was missing.

Fixed in PR #271 . This adds the parameter for pagination and updates tests. Should resolve the `TypeError` you saw. Will be in the next release.

Appreciate the contribution!@fergoid: Thanks I was just going to have a go myself but you beat me to it!

Solution patch:
diff --git a/mypy.ini b/mypy.ini
index e9d4090..3b5cbbf 100644
--- a/mypy.ini
+++ b/mypy.ini
@@ -8,7 +8,7 @@ check_untyped_defs = True
 disallow_untyped_decorators = False
 no_implicit_optional = True
 warn_redundant_casts = True
-warn_unused_ignores = True
+warn_unused_ignores = False
 warn_no_return = True
 warn_unreachable = True
 strict_equality = True
diff --git a/src/mcp_atlassian/jira/epics.py b/src/mcp_atlassian/jira/epics.py
index 4fbf72f..fa7c151 100644
--- a/src/mcp_atlassian/jira/epics.py
+++ b/src/mcp_atlassian/jira/epics.py
@@ -152,7 +152,7 @@ class EpicsMixin(UsersMixin):
             comments = []
             if comment_limit_int is not None:
                 try:
-                    comments_data = self.jira.comments(
+                    comments_data = self.jira.comments(  # type: ignore[attr-defined]
                         issue_key, limit=comment_limit_int
                     )
                     comments = comments_data.get("comments", [])
@@ -671,12 +671,15 @@ class EpicsMixin(UsersMixin):
                 raise Exception(f"Error linking issue to epic: {str(e)}")
             raise
 
-    def get_epic_issues(self, epic_key: str, limit: int = 50) -> list[JiraIssue]:
+    def get_epic_issues(
+        self, epic_key: str, start: int = 0, limit: int = 50
+    ) -> list[JiraIssue]:
         """
         Get all issues linked to a specific epic.
 
         Args:
             epic_key: The key of the epic (e.g. 'PROJ-123')
+            start: Starting index for pagination
             limit: Maximum number of issues to return
 
         Returns:
@@ -721,12 +724,14 @@ class EpicsMixin(UsersMixin):
 
                     # If we have search_issues method available, use it
                     if hasattr(self, "search_issues") and callable(self.search_issues):
-                        issues = self.search_issues(jql, limit=limit)
-                        if issues:
+                        search_result = self.search_issues(
+                            jql, start=start, limit=limit
+                        )
+                        if search_result:
                             logger.info(
-                                f"Successfully found {len(issues)} issues for epic {epic_key} using issueFunction"
+                                f"Successfully found {len(search_result.issues)} issues for epic {epic_key} using issueFunction"
                             )
-                            return issues
+                            return search_result.issues
                 except Exception as e:
                     # Log exception but continue with fallback
                     logger.warning(
@@ -741,7 +746,7 @@ class EpicsMixin(UsersMixin):
                     logger.info(
                         f"Trying to get epic issues with parent relationship: {jql}"
                     )
-                    issues = self._get_epic_issues_by_jql(epic_key, jql, limit)
+                    issues = self._get_epic_issues_by_jql(epic_key, jql, start, limit)
                     if issues:
                         logger.info(
                             f"Successfully found {len(issues)} issues for epic {epic_key} using parent relationship"
@@ -760,7 +765,7 @@ class EpicsMixin(UsersMixin):
                     logger.info(
                         f"Trying to get epic issues with epic link field: {jql}"
                     )
-                    issues = self._get_epic_issues_by_jql(epic_key, jql, limit)
+                    issues = self._get_epic_issues_by_jql(epic_key, jql, start, limit)
                     if issues:
                         logger.info(
                             f"Successfully found {len(issues)} issues for epic {epic_key} using epic link field {epic_link_field}"
@@ -779,7 +784,7 @@ class EpicsMixin(UsersMixin):
                     logger.info(
                         f"Trying to get epic issues with 'Epic Link' field name: {jql}"
                     )
-                    issues = self._get_epic_issues_by_jql(epic_key, jql, limit)
+                    issues = self._get_epic_issues_by_jql(epic_key, jql, start, limit)
                     if issues:
                         logger.info(
                             f"Successfully found {len(issues)} issues for epic {epic_key} using 'Epic Link' field name"
@@ -800,7 +805,9 @@ class EpicsMixin(UsersMixin):
                             f"Trying to get epic issues with issue links: {jql}"
                         )
                         try:
-                            issues = self._get_epic_issues_by_jql(epic_key, jql, limit)
+                            issues = self._get_epic_issues_by_jql(
+                                epic_key, jql, start, limit
+                            )
                             if issues:
                                 logger.info(
                                     f"Successfully found {len(issues)} issues for epic {epic_key} using issue links with type '{link_type}'"
@@ -835,7 +842,9 @@ class EpicsMixin(UsersMixin):
                         logger.info(
                             f"Trying to get epic issues with common field ID: {jql}"
                         )
-                        issues = self._get_epic_issues_by_jql(epic_key, jql, limit)
+                        issues = self._get_epic_issues_by_jql(
+                            epic_key, jql, start, limit
+                        )
                         if issues:
                             logger.info(
                                 f"Successfully found {len(issues)} issues for epic {epic_key} using field ID {field_id}"
@@ -1022,7 +1031,7 @@ class EpicsMixin(UsersMixin):
         return []
 
     def _get_epic_issues_by_jql(
-        self, epic_key: str, jql: str, limit: int
+        self, epic_key: str, jql: str, start: int, limit: int
     ) -> list[JiraIssue]:
         """
         Helper method to get issues using a JQL query.
@@ -1030,6 +1039,7 @@ class EpicsMixin(UsersMixin):
         Args:
             epic_key: The key of the epic
             jql: JQL query to execute
+            start: Starting index for pagination
             limit: Maximum number of issues to return
 
         Returns:
@@ -1037,13 +1047,17 @@ class EpicsMixin(UsersMixin):
         """
         # Try to use search_issues if available
         if hasattr(self, "search_issues") and callable(self.search_issues):
-            issues = self.search_issues(jql, limit=limit)
-            if not issues:
+            search_result = self.search_issues(jql, start=start, limit=limit)
+            if not search_result:
                 logger.warning(f"No issues found for epic {epic_key} with query: {jql}")
-            return issues
+            return (
+                search_result.issues
+                if hasattr(search_result, "issues")
+                else search_result
+            )
         else:
             # Fallback if search_issues is not available
-            issues_data = self.jira.jql(jql, limit=limit)
+            issues_data = self.jira.jql(jql, start=start, limit=limit)
             issues = []
 
             # Create JiraIssue models from raw data
diff --git a/src/mcp_atlassian/jira/fields.py b/src/mcp_atlassian/jira/fields.py
index 868e21e..7190fab 100644
--- a/src/mcp_atlassian/jira/fields.py
+++ b/src/mcp_atlassian/jira/fields.py
@@ -149,7 +149,7 @@ class FieldsMixin(JiraClient):
         """
         try:
             # Create meta provides field requirements for different issue types
-            create_meta = self.jira.createmeta(
+            create_meta = self.jira.createmeta(  # type: ignore[attr-defined]
                 projectKeys=project_key,
                 issuetypeNames=issue_type,
                 expand="projects.issuetypes.fields",
diff --git a/src/mcp_atlassian/jira/issues.py b/src/mcp_atlassian/jira/issues.py
index 29cf5f2..7fb560c 100644
--- a/src/mcp_atlassian/jira/issues.py
+++ b/src/mcp_atlassian/jira/issues.py
@@ -884,7 +884,7 @@ class IssuesMixin(UsersMixin):
 
         # First update any fields if needed
         if fields:
-            self.jira.update_issue(issue_key=issue_key, fields=fields)
+            self.jira.update_issue(issue_key=issue_key, fields=fields)  # type: ignore[call-arg]
 
         # If no status change is requested, return the issue
         if not status:
@@ -1194,7 +1194,7 @@ class IssuesMixin(UsersMixin):
             Exception: If there is an error getting transitions
         """
         try:
-            transitions = self.jira.issue_get_transitions(issue_key)
+            transitions = self.jira.issue_get_transitions(issue_key)  # type: ignore[attr-defined]
             if isinstance(transitions, dict) and "transitions" in transitions:
                 return transitions["transitions"]
             return transitions
diff --git a/src/mcp_atlassian/jira/worklog.py b/src/mcp_atlassian/jira/worklog.py
index 28fd5ea..cfd228a 100644
--- a/src/mcp_atlassian/jira/worklog.py
+++ b/src/mcp_atlassian/jira/worklog.py
@@ -160,7 +160,7 @@ class WorklogMixin(JiraClient):
             Raw worklog data from the API
         """
         try:
-            return self.jira.worklog(issue_key)
+            return self.jira.worklog(issue_key)  # type: ignore[attr-defined]
         except Exception as e:
             logger.warning(f"Error getting worklog for {issue_key}: {e}")
             return {"worklogs": []}


Test patch:
diff --git a/tests/unit/jira/test_epics.py b/tests/unit/jira/test_epics.py
index 3fa520d..fff3da8 100644
--- a/tests/unit/jira/test_epics.py
+++ b/tests/unit/jira/test_epics.py
@@ -552,24 +552,33 @@ class TestEpicsMixin:
             return_value={"epic_link": "customfield_10014"}
         )
 
-        # Mock search_issues to return test data
-        epics_mixin.search_issues = MagicMock(
-            return_value=[
-                JiraIssue(key="TEST-456", summary="Issue 1"),
-                JiraIssue(key="TEST-789", summary="Issue 2"),
-            ]
-        )
+        # Create a mock search result object with issues attribute
+        mock_issues = [
+            JiraIssue(key="TEST-456", summary="Issue 1"),
+            JiraIssue(key="TEST-789", summary="Issue 2"),
+        ]
 
-        # Call the method
-        result = epics_mixin.get_epic_issues("EPIC-123", limit=10)
+        # Create a mock object with an issues attribute
+        class MockSearchResult:
+            def __init__(self, issues):
+                self.issues = issues
+
+        mock_search_result = MockSearchResult(mock_issues)
+
+        # Mock search_issues to return our mock search result
+        epics_mixin.search_issues = MagicMock(return_value=mock_search_result)
+
+        # Call the method with start parameter
+        result = epics_mixin.get_epic_issues("EPIC-123", start=5, limit=10)
 
         # Verify search_issues was called with the right JQL
         epics_mixin.search_issues.assert_called_once()
         call_args = epics_mixin.search_issues.call_args[0]
         assert 'issueFunction in issuesScopedToEpic("EPIC-123")' in call_args[0]
 
-        # Verify keyword arguments for limit
+        # Verify keyword arguments for start and limit
         call_kwargs = epics_mixin.search_issues.call_args[1]
+        assert call_kwargs.get("start") == 5
         assert call_kwargs.get("limit") == 10
 
         # Verify result
@@ -625,28 +634,48 @@ class TestEpicsMixin:
             return_value={"epic_link": "customfield_10014", "parent": "parent"}
         )
 
+        # Create a mock class for SearchResult
+        class MockSearchResult:
+            def __init__(self, issues):
+                self.issues = issues
+
+            def __bool__(self):
+                return bool(self.issues)
+
+            def __len__(self):
+                return len(self.issues)
+
         # Mock search_issues to return empty results for issueFunction but results for epic_link
         def search_side_effect(jql, **kwargs):
             if "issueFunction" in jql:
-                return []  # No results for issueFunction
+                return MockSearchResult([])  # No results for issueFunction
             elif "customfield_10014" in jql:
                 # Return results for customfield query
-                return [
-                    JiraIssue(key="CHILD-1", summary="Child 1"),
-                    JiraIssue(key="CHILD-2", summary="Child 2"),
-                ]
-            return []
+                return MockSearchResult(
+                    [
+                        JiraIssue(key="CHILD-1", summary="Child 1"),
+                        JiraIssue(key="CHILD-2", summary="Child 2"),
+                    ]
+                )
+            return MockSearchResult([])
 
         epics_mixin.search_issues = MagicMock(side_effect=search_side_effect)
 
-        # Call the method
-        result = epics_mixin.get_epic_issues("EPIC-123")
+        # Call the method with start parameter
+        result = epics_mixin.get_epic_issues("EPIC-123", start=3, limit=10)
 
         # Verify we got results from the second query
         assert len(result) == 2
         assert result[0].key == "CHILD-1"
         assert result[1].key == "CHILD-2"
 
+        # Verify the start parameter was passed to search_issues
+        assert epics_mixin.search_issues.call_count >= 2
+        # Check last call (which should be the one that returned results)
+        last_call_kwargs = epics_mixin.search_issues.call_args[1]
+        assert last_call_kwargs.get("start") == 3
+        assert last_call_kwargs.get("limit") == 10
+
     def test_get_epic_issues_no_search_issues(self, epics_mixin):
         """Test get_epic_issues when search_issues method is not available."""
         # Setup mocks
@@ -669,8 +698,8 @@ class TestEpicsMixin:
         ]
         epics_mixin.jira.jql.return_value = {"issues": mock_issues}
 
-        # Call the method
-        result = epics_mixin.get_epic_issues("EPIC-123")
+        # Call the method with start parameter
+        result = epics_mixin.get_epic_issues("EPIC-123", start=7, limit=10)
 
         # Verify the result contains documents created from jql results
         assert len(result) == 2
@@ -679,6 +708,12 @@ class TestEpicsMixin:
         assert result[1].key == "TEST-789"
         assert result[1].summary == "Issue 2"
 
+        # Verify the start parameter was passed to jql
+        epics_mixin.jira.jql.assert_called()
+        call_kwargs = epics_mixin.jira.jql.call_args[1]
+        assert call_kwargs.get("start") == 7
+        assert call_kwargs.get("limit") == 10
+
     def test_get_epic_issues_api_error(self, epics_mixin):
         """Test get_epic_issues with API error."""
         # Setup mocks - simulate API error