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()
```


Discussion:
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'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. 
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



Patch:
diff --git a/src/mcp_atlassian/utils/lifecycle.py b/src/mcp_atlassian/utils/lifecycle.py
index 81883d5a..a37a1b1a 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 007594a9..0857e9ec 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()