Repository: sooperset/mcp-atlassian

Issue:
[Bug]: [mcp-atlassian] Server Disconnected Error (Connection Issue)
### 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

[mcp-atlassian] Server Disconnected Error
`ValueError: I/O operation on closed file.`

### Steps to Reproduce

Run `uvx mcp-atlassian` in the terminal.

### Expected Behavior

Starting MCP server 'Atlassian MCP' with transport 'stdio'

### Actual Behavior

```shell
Server disconnected unexpectedly during execution.
Error message:

ValueError: I/O operation on closed file
(log: [mcp-atlassian] [error] Server disconnected.)
```

### mcp-atlassian Version

0.11.6

### Installation Method

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

### Operating System

Windows

### Python Version

3.13

### Atlassian Instance Type

Jira Cloud

### Client Application

Cladue Desktop

### Additional Context

Refer to the solution below.
I had to temporarily modify the Python code.

1. Modify lifecycle.py code (mcp_atlassian/utils/lifecycle.py)
- Original code :
```
    sys.stdout.flush()
    sys.stderr.flush()
```
- Chage code :(added try/except) : 
```
    # Guard against stdout.flush()
    try:
       if not sys.stdout.closed:
            sys.stdout.flush()
    except ValueError:
        pass
    # Guard against stderr.flush()
    try:
        if not sys.stderr.closed:
            sys.stderr.flush()
    except ValueError:
        pass
```

2. Modify __init__.py code(mcp_atlassian/__init__.py)
- Original code : 
```
   try:
       logger.debug("Starting asyncio event loop...")

        # Create a wrapper coroutine that handles both the server and stdin monitoring
        if final_transport == "stdio":
            asyncio.run(run_with_stdio_monitoring(main_mcp.run_async, run_kwargs))
        else:
            asyncio.run(main_mcp.run_async(**run_kwargs))
    except (KeyboardInterrupt, SystemExit) as e:
        logger.info(f"Server shutdown initiated: {type(e).__name__}")
    except Exception as e:
        logger.error(f"Server encountered an error: {e}", exc_info=True)
        sys.exit(1)
    finally:
        ensure_clean_exit()
```
- Chage code :(removed the condition checking for  'final_transport == "stdio"' and 'run_with_stdio_monitoring()') : 
```
    try:
        logger.debug("Starting asyncio event loop…")

        # Always call run_async in blocking mode regardless of transport
        asyncio.run(main_mcp.run_async(**run_kwargs))

    except (KeyboardInterrupt, SystemExit) as e:
        logger.info(f"Server shutdown initiated: {type(e).__name__}")
    except Exception as e:
        logger.error(f"Server encountered an error: {e}", exc_info=True)
        sys.exit(1)
    finally:
        # Only flush when the server has completely shut down
        ensure_clean_exit()
```

Hints from issue comments:
@tigerdelli: I'm facing the same error. This is fairly new and started popping up only about a couple of days back. What is triggering this issue?@yjbae-sqa: > I'm facing the same error. This is fairly new and started popping up only about a couple of days back. What is triggering this issue?

@tigerdelli  It appears that the cause is that the `MCP-Atlassian` server’s standard input/output streams were unexpectedly closed, triggering a `ValueError` during the flush operation and causing the server to terminate abnormally.
It looks like we’ll have to wait for an official bug fix. @sooperset: Thanks for reporting this issue. I've implemented the quick fix on #533 I'll include this fix in the next release! Please let me know if you have further issue after applying this@albertnerikat: > I'm facing the same error. This is fairly new and started popping up only about a couple of days back. What is triggering this issue?

I am also facing the same problem. Started popping up recently. I am connecting using n8n@yjbae-sqa: > > I'm facing the same error. This is fairly new and started popping up only about a couple of days back. What is triggering this issue?
> 
> I am also facing the same problem. Started popping up recently. I am connecting using n8n

I haven’t used n8n before, but if you’re able to modify the code, you should do so; otherwise, you’ll have to wait for the next n8n release.@albertnerikat: This looks more of a memory leaks issue with the mcp server. I am facing
this challenge recently on this now. Any possibility to check that front?

On Tue, Jun 24, 2025, 10:16 p.m. yjbae ***@***.***> wrote:

> *yjbae-sqa* left a comment (sooperset/mcp-atlassian#532)
> <https://github.com/sooperset/mcp-atlassian/issues/532#issuecomment-3002397089>
>
> I'm facing the same error. This is fairly new and started popping up only
> about a couple of days back. What is triggering this issue?
>
> I am also facing the same problem. Started popping up recently. I am
> connecting using n8n
>
> I haven’t used n8n before, but if you’re able to modify the code, you
> should do so; otherwise, you’ll have to wait for the next n8n release.
>
> —
> Reply to this email directly, view it on GitHub
> <https://github.com/sooperset/mcp-atlassian/issues/532#issuecomment-3002397089>,
> or unsubscribe
> <https://github.com/notifications/unsubscribe-auth/ALOVKRR5RJXN33GSA2FNZLD3FIBANAVCNFSM6AAAAAB7UNSAIKVHI2DSMVQWIX3LMV43OSLTON2WKQ3PNVWWK3TUHMZTAMBSGM4TOMBYHE>
> .
> You are receiving this because you commented.Message ID:
> ***@***.***>
>
@yjbae-sqa: > This looks more of a memory leaks issue with the mcp server. I am facing
> this challenge recently on this now. Any possibility to check that front?
> […](#)

I think we need to check the code to identify the memory leak. It usually happens because there's something wrong in the code.@sooperset: @albertnerikat it's interesting. would you create a new issue about this memory leak? it'd be great to share some logs as well!

Solution patch:
diff --git a/src/mcp_atlassian/utils/lifecycle.py b/src/mcp_atlassian/utils/lifecycle.py
index 81883d5..a37a1b1 100644
--- a/src/mcp_atlassian/utils/lifecycle.py
+++ b/src/mcp_atlassian/utils/lifecycle.py
@@ -49,9 +49,26 @@ def ensure_clean_exit() -> None:
 
     This is important for containerized environments where output might be
     buffered and could be lost if not properly flushed before exit.
+
+    Handles cases where streams may already be closed by the parent process,
+    particularly on Windows or when run as a child process.
     """
     logger.info("Server stopped, flushing output streams...")
-    # Ensure all output is flushed before exit
-    sys.stdout.flush()
-    sys.stderr.flush()
+
+    # Safely flush stdout
+    try:
+        if hasattr(sys.stdout, "closed") and not sys.stdout.closed:
+            sys.stdout.flush()
+    except (ValueError, OSError, AttributeError) as e:
+        # Stream might be closed or redirected
+        logger.debug(f"Could not flush stdout: {e}")
+
+    # Safely flush stderr
+    try:
+        if hasattr(sys.stderr, "closed") and not sys.stderr.closed:
+            sys.stderr.flush()
+    except (ValueError, OSError, AttributeError) as e:
+        # Stream might be closed or redirected
+        logger.debug(f"Could not flush stderr: {e}")
+
     logger.debug("Output streams flushed, exiting gracefully")


Test patch:
diff --git a/tests/unit/utils/test_lifecycle.py b/tests/unit/utils/test_lifecycle.py
index 007594a..0857e9e 100644
--- a/tests/unit/utils/test_lifecycle.py
+++ b/tests/unit/utils/test_lifecycle.py
@@ -81,8 +81,90 @@ class TestEnsureCleanExit:
     @patch("sys.stdout")
     def test_ensure_clean_exit(self, mock_stdout, mock_stderr):
         """Test that output streams are flushed on exit."""
+        # Mock streams as open
+        mock_stdout.closed = False
+        mock_stderr.closed = False
+
         ensure_clean_exit()
 
         # Check both streams were flushed
         mock_stdout.flush.assert_called_once()
         mock_stderr.flush.assert_called_once()
+
+    @patch("sys.stderr")
+    @patch("sys.stdout")
+    def test_ensure_clean_exit_closed_stdout(self, mock_stdout, mock_stderr):
+        """Test that closed stdout is handled gracefully."""
+        # Mock stdout as closed, stderr as open
+        mock_stdout.closed = True
+        mock_stderr.closed = False
+
+        ensure_clean_exit()
+
+        # Check stdout was not flushed
+        mock_stdout.flush.assert_not_called()
+        # Check stderr was still flushed
+        mock_stderr.flush.assert_called_once()
+
+    @patch("sys.stderr")
+    @patch("sys.stdout")
+    def test_ensure_clean_exit_closed_stderr(self, mock_stdout, mock_stderr):
+        """Test that closed stderr is handled gracefully."""
+        # Mock stderr as closed, stdout as open
+        mock_stdout.closed = False
+        mock_stderr.closed = True
+
+        ensure_clean_exit()
+
+        # Check stdout was flushed
+        mock_stdout.flush.assert_called_once()
+        # Check stderr was not flushed
+        mock_stderr.flush.assert_not_called()
+
+    @patch("sys.stderr")
+    @patch("sys.stdout")
+    def test_ensure_clean_exit_both_closed(self, mock_stdout, mock_stderr):
+        """Test that both streams being closed is handled gracefully."""
+        # Mock both streams as closed
+        mock_stdout.closed = True
+        mock_stderr.closed = True
+
+        ensure_clean_exit()
+
+        # Check neither stream was flushed
+        mock_stdout.flush.assert_not_called()
+        mock_stderr.flush.assert_not_called()
+
+    @patch("sys.stderr")
+    @patch("sys.stdout")
+    def test_ensure_clean_exit_flush_raises_value_error(self, mock_stdout, mock_stderr):
+        """Test that ValueError during flush is handled gracefully."""
+        # Mock streams as open but flush raises ValueError
+        mock_stdout.closed = False
+        mock_stderr.closed = False
+        mock_stdout.flush.side_effect = ValueError("I/O operation on closed file")
+        mock_stderr.flush.side_effect = ValueError("I/O operation on closed file")
+
+        # Should not raise exception
+        ensure_clean_exit()
+
+        # Check both streams had flush attempts
+        mock_stdout.flush.assert_called_once()
+        mock_stderr.flush.assert_called_once()
+
+    @patch("sys.stderr")
+    @patch("sys.stdout")
+    def test_ensure_clean_exit_no_closed_attribute(self, mock_stdout, mock_stderr):
+        """Test handling of streams without 'closed' attribute."""
+        # Remove closed attribute to simulate non-standard streams
+        if hasattr(mock_stdout, "closed"):
+            delattr(mock_stdout, "closed")
+        if hasattr(mock_stderr, "closed"):
+            delattr(mock_stderr, "closed")
+
+        # Should not raise exception
+        ensure_clean_exit()
+
+        # Check neither stream was flushed (no closed attribute)
+        mock_stdout.flush.assert_not_called()
+        mock_stderr.flush.assert_not_called()