Skip to content
Merged
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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.
- `cloudsmith credential-helper install` and `uninstall` no longer rewrite a config file when they make no change to its data. Previously, the CLI always rewrote a file in a different formatting, such as the tab-indented `config.json` that Docker writes. The command also made a `.bak` copy and reported changes that it did not make.
- When the API host name or the proxy host name does not exist, for example because of a typo in `--api-host`, commands now fail immediately. Previously, they retried for approximately 62 seconds without output. The CLI still retries a DNS failure that the operating system reports as temporary.
- When the CLI waits before it retries a failed connection or a 5xx response, it now writes `Request failed: retrying in N second(s) ...` to stderr. Previously, it waited without output.
- `cloudsmith mcp start` now stops without an error when the MCP client closes the connection while the server writes a reply. Previously, the command stopped with `BrokenPipeError` and exit status 1.
- The CLI now exits with status 1 when you interrupt it with Ctrl+C after the reader of its stderr has closed the pipe. Before, the CLI sent an error report and exited with status 120.
- `cloudsmith check rates` no longer stops with `TypeError` when the API does not give a value for an interval, limit, remaining count, reset time or throttled status. The command now shows `None` for that value.

Expand Down
38 changes: 38 additions & 0 deletions cloudsmith_cli/cli/tests/commands/test_mcp.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import json
import os
import stat
import subprocess
import sys
from pathlib import Path
from unittest.mock import patch
Expand Down Expand Up @@ -740,3 +741,40 @@ def test_configure_prints_on_cp1252_console(self, tmp_path):
assert result.exit_code == 0, result.output
assert "Configured Cursor" in result.stdout
assert config_path.exists()


def test_server_stops_quietly_when_the_client_closes_stdout():
"""The stdio transport raises the closed pipe inside an ExceptionGroup."""
code = """
import os
from unittest.mock import patch

import cloudsmith_api

from cloudsmith_cli.core.mcp.server import DynamicMCPServer

read_end, write_end = os.pipe()
os.close(read_end)
os.dup2(write_end, 1)
with patch.object(DynamicMCPServer, "load_openapi_spec"):
DynamicMCPServer(api_config=cloudsmith_api.Configuration()).run()
"""
initialize = {
"jsonrpc": "2.0",
"id": 1,
"method": "initialize",
"params": {
"protocolVersion": "2025-06-18",
"capabilities": {},
"clientInfo": {"name": "test", "version": "1"},
},
}
result = subprocess.run(
[sys.executable, "-c", code],
input=json.dumps(initialize) + "\n",
capture_output=True,
text=True,
check=False,
)

assert result.returncode == 0, result.stderr
5 changes: 4 additions & 1 deletion cloudsmith_cli/core/mcp/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -883,7 +883,10 @@ def run(self):
"""Initialize and run the server"""
asyncio.run(self.load_openapi_spec())
try:
self.mcp.run(transport="stdio")
try:
self.mcp.run(transport="stdio")
except* BrokenPipeError:
pass # The MCP client closed the connection.
except asyncio.CancelledError:
print("Server shutdown requested")

Expand Down
Loading