codeocean-mcp-server 0.12.0__tar.gz → 0.12.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (30) hide show
  1. {codeocean_mcp_server-0.12.0 → codeocean_mcp_server-0.12.1}/CHANGELOG.md +4 -0
  2. {codeocean_mcp_server-0.12.0 → codeocean_mcp_server-0.12.1}/PKG-INFO +1 -1
  3. {codeocean_mcp_server-0.12.0 → codeocean_mcp_server-0.12.1}/pyproject.toml +1 -1
  4. {codeocean_mcp_server-0.12.0 → codeocean_mcp_server-0.12.1}/src/codeocean_mcp_server/logging_config.py +23 -3
  5. {codeocean_mcp_server-0.12.0 → codeocean_mcp_server-0.12.1}/src/codeocean_mcp_server/server.py +1 -1
  6. codeocean_mcp_server-0.12.1/tests/test_logging_config.py +68 -0
  7. {codeocean_mcp_server-0.12.0 → codeocean_mcp_server-0.12.1}/uv.lock +1 -1
  8. {codeocean_mcp_server-0.12.0 → codeocean_mcp_server-0.12.1}/.gitignore +0 -0
  9. {codeocean_mcp_server-0.12.0 → codeocean_mcp_server-0.12.1}/.python-version +0 -0
  10. {codeocean_mcp_server-0.12.0 → codeocean_mcp_server-0.12.1}/LICENSE +0 -0
  11. {codeocean_mcp_server-0.12.0 → codeocean_mcp_server-0.12.1}/README.md +0 -0
  12. {codeocean_mcp_server-0.12.0 → codeocean_mcp_server-0.12.1}/RELEASE.md +0 -0
  13. {codeocean_mcp_server-0.12.0 → codeocean_mcp_server-0.12.1}/src/codeocean_mcp_server/__init__.py +0 -0
  14. {codeocean_mcp_server-0.12.0 → codeocean_mcp_server-0.12.1}/src/codeocean_mcp_server/client.py +0 -0
  15. {codeocean_mcp_server-0.12.0 → codeocean_mcp_server-0.12.1}/src/codeocean_mcp_server/file_utils.py +0 -0
  16. {codeocean_mcp_server-0.12.0 → codeocean_mcp_server-0.12.1}/src/codeocean_mcp_server/models.py +0 -0
  17. {codeocean_mcp_server-0.12.0 → codeocean_mcp_server-0.12.1}/src/codeocean_mcp_server/search.py +0 -0
  18. {codeocean_mcp_server-0.12.0 → codeocean_mcp_server-0.12.1}/src/codeocean_mcp_server/tools/__init__.py +0 -0
  19. {codeocean_mcp_server-0.12.0 → codeocean_mcp_server-0.12.1}/src/codeocean_mcp_server/tools/capsules.py +0 -0
  20. {codeocean_mcp_server-0.12.0 → codeocean_mcp_server-0.12.1}/src/codeocean_mcp_server/tools/computations.py +0 -0
  21. {codeocean_mcp_server-0.12.0 → codeocean_mcp_server-0.12.1}/src/codeocean_mcp_server/tools/custom_metadata.py +0 -0
  22. {codeocean_mcp_server-0.12.0 → codeocean_mcp_server-0.12.1}/src/codeocean_mcp_server/tools/data_assets.py +0 -0
  23. {codeocean_mcp_server-0.12.0 → codeocean_mcp_server-0.12.1}/tests/bedrock_call.py +0 -0
  24. {codeocean_mcp_server-0.12.0 → codeocean_mcp_server-0.12.1}/tests/bedrock_tools_converter.py +0 -0
  25. {codeocean_mcp_server-0.12.0 → codeocean_mcp_server-0.12.1}/tests/conftest.py +0 -0
  26. {codeocean_mcp_server-0.12.0 → codeocean_mcp_server-0.12.1}/tests/mcp_client.py +0 -0
  27. {codeocean_mcp_server-0.12.0 → codeocean_mcp_server-0.12.1}/tests/test_http_transport.py +0 -0
  28. {codeocean_mcp_server-0.12.0 → codeocean_mcp_server-0.12.1}/tests/test_multi_tool_calling.py +0 -0
  29. {codeocean_mcp_server-0.12.0 → codeocean_mcp_server-0.12.1}/tests/test_search.py +0 -0
  30. {codeocean_mcp_server-0.12.0 → codeocean_mcp_server-0.12.1}/tests/test_tool_calling.py +0 -0
@@ -1,6 +1,10 @@
1
1
  CHANGELOG
2
2
  =========
3
3
 
4
+ ## 0.12.1 (2026-08-28)
5
+
6
+ - [#40](https://github.com/codeocean/codeocean-mcp-server/pull/40) fix: apply `LOG_FORMAT` to uvicorn's own log records
7
+
4
8
  ## 0.12.0 (2026-08-27)
5
9
 
6
10
  - [#38](https://github.com/codeocean/codeocean-mcp-server/pull/38) feat: serve over streamable HTTP with per-request credentials
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: codeocean-mcp-server
3
- Version: 0.12.0
3
+ Version: 0.12.1
4
4
  Summary: Code Ocean MCP Server
5
5
  Project-URL: Homepage, https://github.com/codeocean/codeocean-mcp-server
6
6
  Project-URL: Issues, https://github.com/codeocean/codeocean-mcp-server/issues
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "codeocean-mcp-server"
3
- version = "0.12.0"
3
+ version = "0.12.1"
4
4
  authors = [{ name = "Code Ocean", email = "dev@codeocean.com" }]
5
5
  description = "Code Ocean MCP Server"
6
6
  readme = "README.md"
@@ -3,16 +3,21 @@ import os
3
3
  import sys
4
4
 
5
5
 
6
- def configure_logging() -> None:
6
+ def configure_logging(transport: str = "stdio") -> None:
7
7
  """Configure logging based on LOG_FORMAT environment variable.
8
8
 
9
9
  If LOG_FORMAT is set, configures the root logger with a StreamHandler
10
- using the specified format string. This must be called before FastMCP
11
- initialization to ensure our configuration takes precedence.
10
+ using the specified format string, and, for a transport uvicorn serves,
11
+ applies the same format to uvicorn's own loggers. This must be called
12
+ before FastMCP initialization to ensure our configuration takes
13
+ precedence.
12
14
 
13
15
  If LOG_FORMAT is not set or is empty, does nothing and lets FastMCP
14
16
  configure logging with its default settings.
15
17
 
18
+ Args:
19
+ transport: The transport the server is about to serve on.
20
+
16
21
  Environment variables:
17
22
  LOG_FORMAT: Python logging format string (optional)
18
23
 
@@ -45,3 +50,18 @@ def configure_logging() -> None:
45
50
  # This must be done before FastMCP calls logging.basicConfig()
46
51
  logging.root.addHandler(handler)
47
52
  logging.root.setLevel(logging.INFO)
53
+
54
+ if transport == "stdio":
55
+ return
56
+
57
+ # Serving over HTTP, uvicorn logs through its own loggers, which it gives handlers that do not
58
+ # propagate to the root logger. It configures them from this dictionary, which FastMCP leaves
59
+ # at its default value, so its contents have to be replaced to be reached at all. The import
60
+ # is local because uvicorn, like in FastMCP itself, is only needed for the transports it serves.
61
+ from uvicorn.config import LOGGING_CONFIG
62
+
63
+ LOGGING_CONFIG["formatters"]["default"]["fmt"] = log_format
64
+ # Access records carry the request in fields of their own rather than in the message.
65
+ LOGGING_CONFIG["formatters"]["access"]["fmt"] = log_format.replace(
66
+ "%(message)s", '%(client_addr)s - "%(request_line)s" %(status_code)s'
67
+ )
@@ -31,8 +31,8 @@ def parse_args(argv: list[str] | None = None) -> argparse.Namespace:
31
31
 
32
32
  def main():
33
33
  """Run the MCP server."""
34
- configure_logging()
35
34
  args = parse_args()
35
+ configure_logging(args.transport)
36
36
  stdio = args.transport == "stdio"
37
37
  domain = os.getenv("CODEOCEAN_DOMAIN")
38
38
  token = os.getenv("CODEOCEAN_TOKEN")
@@ -0,0 +1,68 @@
1
+ """End-to-end test for LOG_FORMAT, over the transport that brings a logger of its own along."""
2
+
3
+ import os
4
+ import re
5
+ import socket
6
+ import subprocess
7
+ import sys
8
+ import time
9
+ import urllib.error
10
+ import urllib.request
11
+ from pathlib import Path
12
+
13
+ SERVER_SCRIPT_PATH = str(Path(__file__).parent.parent / "src" / "codeocean_mcp_server" / "server.py")
14
+ LOG_FORMAT = "%(asctime)s server %(levelname)s [%(name)s] %(message)s"
15
+
16
+
17
+ def _free_port() -> int:
18
+ with socket.socket() as sock:
19
+ sock.bind(("127.0.0.1", 0))
20
+ return sock.getsockname()[1]
21
+
22
+
23
+ def _wait_until_listening(port: int, timeout: float = 30.0):
24
+ deadline = time.monotonic() + timeout
25
+ while time.monotonic() < deadline:
26
+ with socket.socket() as sock:
27
+ if sock.connect_ex(("127.0.0.1", port)) == 0:
28
+ return
29
+ time.sleep(0.1)
30
+ raise TimeoutError(f"MCP server did not start listening on port {port}")
31
+
32
+
33
+ def _serve_and_request(port: int) -> str:
34
+ """Serve over streamable HTTP, make one request, and return everything the server logged."""
35
+ env = {**os.environ, "CODEOCEAN_DOMAIN": "test-domain", "LOG_FORMAT": LOG_FORMAT}
36
+ process = subprocess.Popen(
37
+ [sys.executable, SERVER_SCRIPT_PATH, "--transport", "streamable-http", "--port", str(port)],
38
+ env=env,
39
+ stdout=subprocess.PIPE,
40
+ stderr=subprocess.STDOUT,
41
+ text=True,
42
+ )
43
+ try:
44
+ _wait_until_listening(port)
45
+ request = urllib.request.Request(f"http://127.0.0.1:{port}/mcp", data=b"{}")
46
+ try:
47
+ urllib.request.urlopen(request, timeout=30)
48
+ except urllib.error.HTTPError:
49
+ pass # The request is rejected; it is logged either way, which is what is under test.
50
+ finally:
51
+ process.terminate()
52
+ process.wait(timeout=30)
53
+ return process.stdout.read()
54
+
55
+
56
+ def test_log_format_applies_to_uvicorns_own_records():
57
+ """Uvicorn brings its own logging configuration, in whose format these records would be."""
58
+ log = _serve_and_request(_free_port())
59
+
60
+ timestamp = r"^[\d-]{10} [\d:,]+"
61
+ assert re.search(rf"{timestamp} server INFO \[uvicorn\.error\] Uvicorn running", log, re.MULTILINE)
62
+ assert re.search(
63
+ # The status runs to the end of the line as code and phrase, which only uvicorn's own
64
+ # access formatter puts there; the raw record carries the code alone.
65
+ rf'{timestamp} server INFO \[uvicorn\.access\] 127\.0\.0\.1:\d+ - "POST /mcp HTTP/1.1" \d{{3}} \w+( \w+)*$',
66
+ log,
67
+ re.MULTILINE,
68
+ )
@@ -558,7 +558,7 @@ wheels = [
558
558
 
559
559
  [[package]]
560
560
  name = "codeocean-mcp-server"
561
- version = "0.12.0"
561
+ version = "0.12.1"
562
562
  source = { editable = "." }
563
563
  dependencies = [
564
564
  { name = "codeocean" },