Repository: sooperset/mcp-atlassian

Issue:
[Bug]: Multi-Cloud OAuth mode: cloud_id not recognized despite being sent in header.
### 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

I’m trying to run the MCP Atlassian server using the Multi-Cloud OAuth mode as documented here:
https://github.com/sooperset/mcp-atlassian#multi-cloud-oauth-support.

### Steps to Reproduce

1 - Cloned the repo.
2 - run server using `uvx mcp-atlassian --transport streamable-http --port 9000 -vv`

My client sends the Authorization and X-Atlassian-Cloud-Id headers as shown:

```
async with streamablehttp_client(
    "http://localhost:9000/mcp/",
    headers={
        "Authorization": f"Bearer {user_token}",
        "X-Atlassian-Cloud-Id": user_cloud_id
    }
) as (read_stream, write_stream, _):
    async with ClientSession(read_stream, write_stream) as session:
        await session.initialize()
        await session.call_tool("jira_get_user_profile", {"user_identifier": "<my-email>"})
```

✅ The server starts fine.
✅ The client connects successfully.
❌ But when calling any Jira tool (e.g., jira_get_user_profile), I get the following error:

in client console:
`Result: meta=None content=[TextContent(type='text', text="Error calling tool 'get_user_profile'", annotations=None)] isError=True`

in mcp-atlassian server console:

`ValueError: Global OAuth config (with cloud_id) for JiraConfig is missing, but user auth_type is 'oauth'. Cannot determine cloud_id.`


### Expected Behavior

tool should be exected properly!

### Actual Behavior

```shell
❌ But when calling any Jira tool (e.g., jira_get_user_profile), I get the following error:

in client console:
`Result: meta=None content=[TextContent(type='text', text="Error calling tool 'get_user_profile'", annotations=None)] isError=True`

in mcp-atlassian server console:

`ValueError: Global OAuth config (with cloud_id) for JiraConfig is missing, but user auth_type is 'oauth'. Cannot determine cloud_id.`
```

### mcp-atlassian Version

latest

### Installation Method

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

### Operating System

macOS

### Python Version

3.10.0

### Atlassian Instance Type

Jira Cloud

### Client Application

Script that is mentioned in readme.

### Additional Context

_No response_


Discussion:
Hi @MuhammadAli-208, byot PR #471 was merged, I haven't released a new version with this yet. if you want to try, you can build the docker image with the current main version
thanks @sooperset!

After pulling the latest main branch and building the Docker image:

`docker build -t mcp-jira .`

I ran the server using:

`docker run -e ATLASSIAN_OAUTH_ENABLE=true -p 9000:9000 mcp-jira --transport streamable-http --port 9000 -vv
`

Then I tested the client script:

```
import asyncio
from mcp.client.streamable_http import streamablehttp_client
from mcp import ClientSession

user_token = "my-token"         # REPLACE with actual token
user_cloud_id = "cloud-id"      # REPLACE with actual cloud ID

async def main():
    async with streamablehttp_client(
        "http://localhost:9000/mcp",
        headers={
            "Authorization": f"Bearer {user_token}",
            "X-Atlassian-Cloud-Id": user_cloud_id,
        },
    ) as (read_stream, write_stream, _):
        async with ClientSession(read_stream, write_stream) as session:
            await session.initialize()
            result = await session.call_tool(
                "jira_get_user_profile",
                {
                    "user_identifier": "test@domain.com"
                },
            )
            print("Result:", result)

asyncio.run(main())
```

Docker running server logs:

```
2025-06-25 10:54:30 INFO - mcp-jira - Jira authentication successful. Current user: [REDACTED_NAME] ([REDACTED_EMAIL])
2025-06-25 10:54:30 DEBUG - mcp-jira - Calling self.jira.myself() to get current user details for account ID.
2025-06-25 10:54:30 DEBUG - atlassian.rest_client - curl --silent -X GET -H 'Content-Type: application/json' -H 'Accept: application/json' 'https://api.atlassian.com/ex/jira/[REDACTED_CLOUD_ID]/rest/api/2/myself'
2025-06-25 10:54:31 DEBUG - urllib3.connectionpool - https://api.atlassian.com:443 "GET /ex/jira/[REDACTED_CLOUD_ID]/rest/api/2/myself HTTP/1.1" 200 None
2025-06-25 10:54:31 DEBUG - atlassian.rest_client - HTTP: GET rest/api/2/myself -> 200 OK
2025-06-25 10:54:31 DEBUG - atlassian.rest_client - HTTP: Response text -> {"self":"https://api.atlassian.com/ex/jira/[REDACTED_CLOUD_ID]/rest/api/2/user?accountId=[REDACTED_ACCOUNT_ID]", "accountId":"[REDACTED_ACCOUNT_ID]", "emailAddress":"[REDACTED_EMAIL]", ...}
2025-06-25 10:54:31 DEBUG - mcp-jira - Received myself_data: {... 'accountId': '[REDACTED_ACCOUNT_ID]', 'emailAddress': '[REDACTED_EMAIL]', ...}
2025-06-25 10:54:31 DEBUG - mcp-atlassian.servers.dependencies - get_jira_fetcher: Validated Jira token for user ID: [REDACTED_ACCOUNT_ID]
2025-06-25 10:54:31 DEBUG - mcp-jira - Determined param: username='[REDACTED_EMAIL]'
2025-06-25 10:54:31 DEBUG - mcp-jira - Calling self.jira.user() with parameters: {'username': '[REDACTED_EMAIL]'}
2025-06-25 10:54:31 DEBUG - atlassian.rest_client - curl --silent -X GET -H 'Content-Type: application/json' -H 'Accept: application/json' 'https://api.atlassian.com/ex/jira/[REDACTED_CLOUD_ID]/rest/api/2/user?username=[REDACTED_EMAIL_ENCODED]'
2025-06-25 10:54:33 DEBUG - urllib3.connectionpool - https://api.atlassian.com:443 "GET /ex/jira/[REDACTED_CLOUD_ID]/rest/api/2/user?username=[REDACTED_EMAIL_ENCODED] HTTP/1.1" 400 None
2025-06-25 10:54:33 DEBUG - atlassian.rest_client - HTTP: GET rest/api/2/user -> 400 Bad Request
2025-06-25 10:54:33 DEBUG - atlassian.rest_client - HTTP: Response text -> {"errorMessages":["The 'accountId' query parameter needs to be provided"],"errors":{}}
2025-06-25 10:54:33 ERROR - mcp-jira - HTTP error 400 for '[REDACTED_EMAIL]': The 'accountId' query parameter needs to be provided. Response: ...
2025-06-25 10:54:33 ERROR - mcp_atlassian.servers.jira - Unexpected error in get_user_profile for '[REDACTED_EMAIL]':
...
2025-06-25 10:54:33 Exception: API error getting user profile for '[REDACTED_EMAIL]': The 'accountId' query parameter needs to be provided
2025-06-25 10:54:33 ERROR - mcp_atlassian.servers.jira - get_user_profile failed for '[REDACTED_EMAIL]': An unexpected error occurred while fetching the user profile.
...
2025-06-25 10:54:33 DEBUG - sse_starlette.sse - chunk: b'event: message\r\ndata: {"jsonrpc":"2.0","id":1,"result":{"content":[{"type":"text","text":"{\\n  \\"success\\": false,\\n  \\"error\\": \\"API error getting user profile for \'[REDACTED_EMAIL]\': The \'accountId\' query parameter needs to be provided\\",\\n  \\"user_identifier\\": \\"[REDACTED_EMAIL]\\"\\n}"}],"isError":false}}\r\n\r\n'
2025-06-25 12:02:17 DEBUG - mcp.server.streamable_http_manager - Session already exists, handling request directly
2025-06-25 12:02:17 INFO - mcp.server.streamable_http - Terminating session: 84c9d5bf34ee4626996b9ec17f0d90f5
2025-06-25 12:02:17 DEBUG - mcp.server.streamable_http - Closing standalone SSE writer
2025-06-25 12:02:17 DEBUG - mcp-atlassian.server.main - UserTokenMiddleware.dispatch: EXITED for request path='/mcp/'
2025-06-25 12:02:17 INFO:     192.168.65.1:56532 - "DELETE /mcp/ HTTP/1.1" 200 OK
2025-06-25 12:02:17 INFO - mcp-atlassian.server.main - Main Atlassian MCP server lifespan shutting down...
2025-06-25 12:02:17 DEBUG - mcp-atlassian.server.main - Cleaning up Jira resources...
2025-06-25 12:02:17 DEBUG - mcp-atlassian.server.main - Cleaning up Confluence resources...
2025-06-25 12:02:17 INFO - mcp-atlassian.server.main - Main Atlassian MCP server lifespan shutdown complete.
...
```

Expected Behavior:
It should work correctly, return a proper result, and the server should not shut down unexpectedly.

Request for clarification or fix:
	•	Is this a known issue with multi-cloud OAuth mode?
	•	Is there any workaround or patch I should apply? 

@MuhammadAli-208 Thanks for the detailed log! No, I haven't released this version yet, so you're the first to report it.
I've checked and found an issue with branching between cloud and data center, I'll fix it soon!
Hi, @MuhammadAli-208 I've resolved this issue in PR #561 if you still have the same issue, please re-open this ticket!
@sooperset Thanks for quick update. just tested.

now jira_get_issue working fine.     
```
            result = await session.call_tool(
                "jira_get_issue",
                {"issue_key": "ABSDEVS-1"}
            )
```
but still it's giving error when calling jira_get_user_profile tool.
```
            response = await session.call_tool(
                "jira_get_user_profile",
                {"user_identifier": "test@domain.com"},
            )
```

error is:

`meta=None content=[TextContent(type='text', text='{\n  "success": false,\n  "error": "API error getting user profile for \'test@domain.com\': The \'accountId\' query parameter needs to be provided",\n  "user_identifier": "test@domain.com"\n}', annotations=None)] isError=False`


note: unable to re-open this ticket.



Patch:
diff --git a/src/mcp_atlassian/jira/config.py b/src/mcp_atlassian/jira/config.py
index 3b1fd633..323e89a0 100644
--- a/src/mcp_atlassian/jira/config.py
+++ b/src/mcp_atlassian/jira/config.py
@@ -45,7 +45,17 @@ def is_cloud(self) -> bool:
             True if this is a cloud instance (atlassian.net), False otherwise.
             Localhost URLs are always considered non-cloud (Server/Data Center).
         """
-        return is_atlassian_cloud_url(self.url)
+        # Multi-Cloud OAuth mode: URL might be None, but we use api.atlassian.com
+        if (
+            self.auth_type == "oauth"
+            and self.oauth_config
+            and self.oauth_config.cloud_id
+        ):
+            # OAuth with cloud_id uses api.atlassian.com which is always Cloud
+            return True
+
+        # For other auth types, check the URL
+        return is_atlassian_cloud_url(self.url) if self.url else False
 
     @property
     def verify_ssl(self) -> bool:


Test patch:
diff --git a/tests/unit/jira/test_config.py b/tests/unit/jira/test_config.py
index 19fc1655..a0c2815d 100644
--- a/tests/unit/jira/test_config.py
+++ b/tests/unit/jira/test_config.py
@@ -179,3 +179,27 @@ def test_from_env_proxy_settings():
         assert config.https_proxy == "https://jira-proxy.example.com:8443"
         assert config.socks_proxy == "socks5://user:pass@jira-proxy.example.com:1080"
         assert config.no_proxy == "localhost,127.0.0.1,.internal.example.com"
+
+
+def test_is_cloud_oauth_with_cloud_id():
+    """Test that is_cloud returns True for OAuth with cloud_id regardless of URL."""
+    from mcp_atlassian.utils.oauth import BYOAccessTokenOAuthConfig
+
+    # OAuth with cloud_id and no URL - should be Cloud
+    oauth_config = BYOAccessTokenOAuthConfig(
+        cloud_id="test-cloud-id", access_token="test-token"
+    )
+    config = JiraConfig(
+        url=None,  # URL can be None in Multi-Cloud OAuth mode
+        auth_type="oauth",
+        oauth_config=oauth_config,
+    )
+    assert config.is_cloud is True
+
+    # OAuth with cloud_id and server URL - should still be Cloud
+    config = JiraConfig(
+        url="https://jira.example.com",  # Server-like URL
+        auth_type="oauth",
+        oauth_config=oauth_config,
+    )
+    assert config.is_cloud is True