Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 33 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -142,3 +142,36 @@ CONFLUENCE_URL=https://your-company.atlassian.net/wiki

# Confluence-specific custom headers.
#CONFLUENCE_CUSTOM_HEADERS=X-Confluence-Service=mcp-integration,X-Custom-Auth=confluence-token,X-ALB-Token=secret-token

# =============================================
# INTEGRATION TESTING CONFIGURATION
# =============================================
# These variables are used for integration testing with real Atlassian instances.
# They are optional and only needed when running tests with --use-real-data flag.

# --- Test Data Configuration ---
# Specific test issue key for integration tests
#JIRA_TEST_ISSUE_KEY=PROJ-123

# Specific test epic key for epic-related integration tests
#JIRA_TEST_EPIC_KEY=PROJ-456

# Specific test project key for project-specific integration tests
#JIRA_TEST_PROJECT_KEY=PROJ

# Specific test board ID for agile/sprint integration tests
#JIRA_TEST_BOARD_ID=1000

# Specific test sprint ID for sprint-related integration tests
#JIRA_TEST_SPRINT_ID=10001

# --- Confluence Test Configuration ---
# Specific test page ID for Confluence integration tests
#CONFLUENCE_TEST_PAGE_ID=123456789

# Specific test space key for Confluence integration tests
#CONFLUENCE_TEST_SPACE_KEY=DEV

# --- Proxy Testing Configuration ---
# Test proxy URL for proxy-related integration tests
#TEST_PROXY_URL=http://test-proxy.example.com:8080
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -75,3 +75,7 @@ playground/

# Claude
.claude/

# Amazon Q
.amazonq/
AmazonQ.md
50 changes: 19 additions & 31 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,12 @@ dependencies = [
"beautifulsoup4>=4.12.3",
"httpx>=0.28.0",
"mcp>=1.8.0,<2.0.0",
"fastmcp>=2.3.4,<2.4.0",
"fastmcp>=2.9.0,<3.0.0",
"python-dotenv>=1.0.1",
"markdownify>=0.11.6",
"markdown>=3.7.0",
"markdown-to-confluence>=0.3.0,<0.4.0",
"pydantic>=2.10.6",
"trio>=0.29.0",
"click>=8.1.7",
"uvicorn>=0.27.1",
"starlette>=0.37.1",
Expand Down Expand Up @@ -99,40 +98,29 @@ line-ending = "auto"

[tool.mypy]
python_version = "3.10"
warn_return_any = true
warn_unused_configs = true
disallow_untyped_defs = true
disallow_incomplete_defs = true
check_untyped_defs = true
disallow_untyped_decorators = false
no_implicit_optional = true
warn_redundant_casts = true
ignore_missing_imports = true
follow_imports = "silent"
allow_untyped_defs = true
allow_incomplete_defs = true
allow_untyped_calls = true
allow_untyped_decorators = true
no_implicit_optional = false
strict_optional = false
warn_return_any = false
warn_unused_ignores = false
warn_no_return = true
warn_unreachable = true
strict_equality = true
strict_optional = true
disallow_subclassing_any = true
warn_incomplete_stub = true
exclude = "^src/"
explicit_package_bases = true

[[tool.mypy.overrides]]
module = "tests.*"
disallow_untyped_defs = false
warn_redundant_casts = false
warn_no_return = false
warn_unreachable = false
check_untyped_defs = false
disallow_any_generics = false
disallow_subclassing_any = false
explicit_package_bases = true

[[tool.mypy.overrides]]
module = "atlassian.*"
ignore_missing_imports = true

[[tool.mypy.overrides]]
module = "markdownify.*"
ignore_missing_imports = true

[[tool.mypy.overrides]]
module = "src.mcp_atlassian.*"
disallow_untyped_defs = false
[tool.pytest.ini_options]
asyncio_mode = "auto"
asyncio_default_fixture_loop_scope = "function"

[tool.hatch.version]
source = "uv-dynamic-versioning"
Expand Down
6 changes: 4 additions & 2 deletions src/mcp_atlassian/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -306,6 +306,8 @@ def was_option_provided(ctx: click.Context, param_name: str) -> bool:
if click_ctx and was_option_provided(click_ctx, "jira_projects_filter"):
os.environ["JIRA_PROJECTS_FILTER"] = jira_projects_filter

from fastmcp import settings as fastmcp_settings

from mcp_atlassian.servers import main_mcp

run_kwargs = {
Expand All @@ -325,9 +327,9 @@ def was_option_provided(ctx: click.Context, param_name: str) -> bool:
log_display_path = final_path
if log_display_path is None:
if final_transport == "sse":
log_display_path = main_mcp.settings.sse_path or "/sse"
log_display_path = fastmcp_settings.sse_path or "/sse"
else:
log_display_path = main_mcp.settings.streamable_http_path or "/mcp"
log_display_path = fastmcp_settings.streamable_http_path or "/mcp"

logger.info(
f"Starting server with {final_transport.upper()} transport on http://{final_host}:{final_port}{log_display_path}"
Expand Down
46 changes: 23 additions & 23 deletions src/mcp_atlassian/jira/epics.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,21 +44,17 @@ def _try_discover_fields_from_existing_epic(

# Find an Epic in the system
epics_jql = "issuetype = Epic ORDER BY created DESC"
results = self.jira.jql(epics_jql, fields="*all", limit=1)
if not isinstance(results, dict):
msg = f"Unexpected return value type from `jira.jql`: {type(results)}"
logger.error(msg)
raise TypeError(msg)
results = self.search_issues(epics_jql, fields="*all", limit=1)

# If no epics found, we can't use this method
if not results or not results.get("issues"):
if not results or not results.issues:
logger.warning("No existing Epics found to analyze field structure")
return

# Get the most recent Epic
epic = results["issues"][0]
fields = epic.get("fields", {})
logger.debug(f"Found existing Epic {epic.get('key')} to analyze")
epic = results.issues[0]
fields = epic.custom_fields
logger.debug(f"Found existing Epic {epic.key} to analyze")

# Look for Epic Name and other Epic fields
for field_id, value in fields.items():
Expand Down Expand Up @@ -780,14 +776,14 @@ def _find_sample_epic(self) -> list[dict]:
try:
# Search for issues with type=Epic
jql = "issuetype = Epic ORDER BY updated DESC"
response = self.jira.jql(jql, limit=1)
if not isinstance(response, dict):
msg = f"Unexpected return value type from `jira.jql`: {type(response)}"
logger.error(msg)
raise TypeError(msg)
result = self.search_issues(jql, limit=1)

if response and "issues" in response and response["issues"]:
return response["issues"]
if result and result.issues:
# Convert JiraIssue model back to dict format for compatibility
issue = result.issues[0]
return [
{"key": issue.key, "id": issue.id, "fields": issue.custom_fields}
]
except Exception as e:
logger.warning(f"Error finding sample epic: {str(e)}")
return []
Expand All @@ -811,13 +807,17 @@ def _find_issues_linked_to_epic(self, epic_key: str) -> list[dict]:
f"issueFunction in issuesScopedToEpic('{epic_key}')",
]:
try:
response = self.jira.jql(query, limit=5)
if not isinstance(response, dict):
msg = f"Unexpected return value type from `jira.jql`: {type(response)}"
logger.error(msg)
raise TypeError(msg)
if response.get("issues"):
return response["issues"]
result = self.search_issues(query, limit=5)
if result and result.issues:
# Convert JiraIssue models back to dict format for compatibility
return [
{
"key": issue.key,
"id": issue.id,
"fields": issue.custom_fields,
}
for issue in result.issues
]
except Exception:
# Try next query format
continue
Expand Down
16 changes: 12 additions & 4 deletions src/mcp_atlassian/jira/issues.py
Original file line number Diff line number Diff line change
Expand Up @@ -208,6 +208,7 @@ def get_issue(
issue,
base_url=self.config.url if hasattr(self, "config") else None,
requested_fields=fields,
is_cloud=self.config.is_cloud,
)
except HTTPError as http_err:
if http_err.response is not None and http_err.response.status_code in [
Expand Down Expand Up @@ -645,7 +646,9 @@ def create_issue(
msg = f"Unexpected return value type from `jira.get_issue`: {type(issue_data)}"
logger.error(msg)
raise TypeError(msg)
return JiraIssue.from_api_response(issue_data)
return JiraIssue.from_api_response(
issue_data, is_cloud=self.config.is_cloud
)

except Exception as e:
self._handle_create_issue_error(e, issue_type)
Expand Down Expand Up @@ -1080,7 +1083,9 @@ def update_issue(
msg = f"Unexpected return value type from `jira.get_issue`: {type(issue_data)}"
logger.error(msg)
raise TypeError(msg)
issue = JiraIssue.from_api_response(issue_data)
issue = JiraIssue.from_api_response(
issue_data, is_cloud=self.config.is_cloud
)

# Add attachment results to the response if available
if attachments_result:
Expand Down Expand Up @@ -1123,7 +1128,9 @@ def _update_issue_with_status(
msg = f"Unexpected return value type from `jira.get_issue`: {type(issue_data)}"
logger.error(msg)
raise TypeError(msg)
return JiraIssue.from_api_response(issue_data)
return JiraIssue.from_api_response(
issue_data, is_cloud=self.config.is_cloud
)

# Get available transitions (uses TransitionsMixin's normalized implementation)
transitions = self.get_available_transitions(issue_key) # type: ignore[attr-defined]
Expand Down Expand Up @@ -1222,7 +1229,7 @@ def _update_issue_with_status(
msg = f"Unexpected return value type from `jira.get_issue`: {type(issue_data)}"
logger.error(msg)
raise TypeError(msg)
return JiraIssue.from_api_response(issue_data)
return JiraIssue.from_api_response(issue_data, is_cloud=self.config.is_cloud)

def delete_issue(self, issue_key: str) -> bool:
"""
Expand Down Expand Up @@ -1466,6 +1473,7 @@ def batch_create_issues(
base_url=self.config.url
if hasattr(self, "config")
else None,
is_cloud=self.config.is_cloud,
)
)
except Exception as e:
Expand Down
Loading
Loading