codeocean-mcp-server 0.12.1__tar.gz → 0.12.2__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.1 → codeocean_mcp_server-0.12.2}/CHANGELOG.md +4 -0
  2. {codeocean_mcp_server-0.12.1 → codeocean_mcp_server-0.12.2}/PKG-INFO +2 -1
  3. {codeocean_mcp_server-0.12.1 → codeocean_mcp_server-0.12.2}/pyproject.toml +2 -1
  4. {codeocean_mcp_server-0.12.1 → codeocean_mcp_server-0.12.2}/src/codeocean_mcp_server/server.py +33 -1
  5. {codeocean_mcp_server-0.12.1 → codeocean_mcp_server-0.12.2}/tests/test_http_transport.py +22 -0
  6. {codeocean_mcp_server-0.12.1 → codeocean_mcp_server-0.12.2}/uv.lock +3 -1
  7. {codeocean_mcp_server-0.12.1 → codeocean_mcp_server-0.12.2}/.gitignore +0 -0
  8. {codeocean_mcp_server-0.12.1 → codeocean_mcp_server-0.12.2}/.python-version +0 -0
  9. {codeocean_mcp_server-0.12.1 → codeocean_mcp_server-0.12.2}/LICENSE +0 -0
  10. {codeocean_mcp_server-0.12.1 → codeocean_mcp_server-0.12.2}/README.md +0 -0
  11. {codeocean_mcp_server-0.12.1 → codeocean_mcp_server-0.12.2}/RELEASE.md +0 -0
  12. {codeocean_mcp_server-0.12.1 → codeocean_mcp_server-0.12.2}/src/codeocean_mcp_server/__init__.py +0 -0
  13. {codeocean_mcp_server-0.12.1 → codeocean_mcp_server-0.12.2}/src/codeocean_mcp_server/client.py +0 -0
  14. {codeocean_mcp_server-0.12.1 → codeocean_mcp_server-0.12.2}/src/codeocean_mcp_server/file_utils.py +0 -0
  15. {codeocean_mcp_server-0.12.1 → codeocean_mcp_server-0.12.2}/src/codeocean_mcp_server/logging_config.py +0 -0
  16. {codeocean_mcp_server-0.12.1 → codeocean_mcp_server-0.12.2}/src/codeocean_mcp_server/models.py +0 -0
  17. {codeocean_mcp_server-0.12.1 → codeocean_mcp_server-0.12.2}/src/codeocean_mcp_server/search.py +0 -0
  18. {codeocean_mcp_server-0.12.1 → codeocean_mcp_server-0.12.2}/src/codeocean_mcp_server/tools/__init__.py +0 -0
  19. {codeocean_mcp_server-0.12.1 → codeocean_mcp_server-0.12.2}/src/codeocean_mcp_server/tools/capsules.py +0 -0
  20. {codeocean_mcp_server-0.12.1 → codeocean_mcp_server-0.12.2}/src/codeocean_mcp_server/tools/computations.py +0 -0
  21. {codeocean_mcp_server-0.12.1 → codeocean_mcp_server-0.12.2}/src/codeocean_mcp_server/tools/custom_metadata.py +0 -0
  22. {codeocean_mcp_server-0.12.1 → codeocean_mcp_server-0.12.2}/src/codeocean_mcp_server/tools/data_assets.py +0 -0
  23. {codeocean_mcp_server-0.12.1 → codeocean_mcp_server-0.12.2}/tests/bedrock_call.py +0 -0
  24. {codeocean_mcp_server-0.12.1 → codeocean_mcp_server-0.12.2}/tests/bedrock_tools_converter.py +0 -0
  25. {codeocean_mcp_server-0.12.1 → codeocean_mcp_server-0.12.2}/tests/conftest.py +0 -0
  26. {codeocean_mcp_server-0.12.1 → codeocean_mcp_server-0.12.2}/tests/mcp_client.py +0 -0
  27. {codeocean_mcp_server-0.12.1 → codeocean_mcp_server-0.12.2}/tests/test_logging_config.py +0 -0
  28. {codeocean_mcp_server-0.12.1 → codeocean_mcp_server-0.12.2}/tests/test_multi_tool_calling.py +0 -0
  29. {codeocean_mcp_server-0.12.1 → codeocean_mcp_server-0.12.2}/tests/test_search.py +0 -0
  30. {codeocean_mcp_server-0.12.1 → codeocean_mcp_server-0.12.2}/tests/test_tool_calling.py +0 -0
@@ -1,6 +1,10 @@
1
1
  CHANGELOG
2
2
  =========
3
3
 
4
+ ## 0.12.2 (2026-09-03)
5
+
6
+ - [#42](https://github.com/codeocean/codeocean-mcp-server/pull/42) fix: run synchronous tool bodies in a worker thread
7
+
4
8
  ## 0.12.1 (2026-08-28)
5
9
 
6
10
  - [#40](https://github.com/codeocean/codeocean-mcp-server/pull/40) fix: apply `LOG_FORMAT` to uvicorn's own log records
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: codeocean-mcp-server
3
- Version: 0.12.1
3
+ Version: 0.12.2
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
@@ -12,6 +12,7 @@ Classifier: License :: OSI Approved :: MIT License
12
12
  Classifier: Operating System :: OS Independent
13
13
  Classifier: Programming Language :: Python :: 3
14
14
  Requires-Python: >=3.10
15
+ Requires-Dist: anyio>=4.5
15
16
  Requires-Dist: codeocean<0.15.0,>=0.14.0
16
17
  Requires-Dist: mcp<1.24.0,>=1.23.0
17
18
  Description-Content-Type: text/markdown
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "codeocean-mcp-server"
3
- version = "0.12.1"
3
+ version = "0.12.2"
4
4
  authors = [{ name = "Code Ocean", email = "dev@codeocean.com" }]
5
5
  description = "Code Ocean MCP Server"
6
6
  readme = "README.md"
@@ -11,6 +11,7 @@ classifiers = [
11
11
  "Operating System :: OS Independent",
12
12
  ]
13
13
  dependencies = [
14
+ "anyio>=4.5",
14
15
  "codeocean>=0.14.0,<0.15.0",
15
16
  "mcp>=1.23.0,<1.24.0",
16
17
  ]
@@ -1,6 +1,10 @@
1
1
  import argparse
2
+ import functools
3
+ import inspect
2
4
  import os
5
+ from collections.abc import Callable
3
6
 
7
+ import anyio
4
8
  from codeocean import CodeOcean
5
9
  from mcp.server.fastmcp import FastMCP
6
10
 
@@ -14,6 +18,34 @@ from codeocean_mcp_server.tools import (
14
18
  )
15
19
 
16
20
 
21
+ class ThreadedFastMCP(FastMCP):
22
+ """A server that runs synchronous tool bodies in a worker thread.
23
+
24
+ FastMCP awaits an asynchronous tool but calls a synchronous one on the event loop, where it
25
+ blocks the whole process for as long as it runs: no other tool call, and no other session's
26
+ requests, are served meanwhile. The tools here are synchronous and some wait on a computation,
27
+ so they are handed to a thread instead. anyio copies the context into it, leaving each call's
28
+ per-request credentials in place.
29
+ """
30
+
31
+ def tool(self, *args, **kwargs) -> Callable[[Callable], Callable]:
32
+ """Register a tool, moving a synchronous one off the event loop."""
33
+ register = super().tool(*args, **kwargs)
34
+
35
+ def decorator(fn: Callable) -> Callable:
36
+ if inspect.iscoroutinefunction(fn):
37
+ return register(fn)
38
+
39
+ @functools.wraps(fn)
40
+ async def in_worker_thread(**arguments):
41
+ return await anyio.to_thread.run_sync(functools.partial(fn, **arguments))
42
+
43
+ register(in_worker_thread)
44
+ return fn
45
+
46
+ return decorator
47
+
48
+
17
49
  def parse_args(argv: list[str] | None = None) -> argparse.Namespace:
18
50
  """Parse command line arguments."""
19
51
  parser = argparse.ArgumentParser(prog="codeocean-mcp-server", description="Code Ocean MCP Server")
@@ -40,7 +72,7 @@ def main():
40
72
  raise ValueError("Environment variable CODEOCEAN_DOMAIN must be set.")
41
73
  agent_id = os.getenv("AGENT_ID", "AI Agent")
42
74
 
43
- mcp = FastMCP(
75
+ mcp = ThreadedFastMCP(
44
76
  name="Code Ocean",
45
77
  instructions=(
46
78
  f"MCP server for Code Ocean: search & run capsules, pipelines, and assets using Code Ocean domain {domain}."
@@ -23,12 +23,17 @@ from mcp_client import get_tools
23
23
 
24
24
  SERVER_SCRIPT_PATH = str(Path(__file__).parent.parent / "src" / "codeocean_mcp_server" / "server.py")
25
25
 
26
+ # How long the stub API takes to answer a token named "slow-...", standing in for a tool that waits.
27
+ SLOW_API_SECONDS = 3.0
28
+
26
29
 
27
30
  class _EchoTokenHandler(BaseHTTPRequestHandler):
28
31
  """Answer the custom metadata endpoint with the basic-auth user, which is the API token."""
29
32
 
30
33
  def do_GET(self): # noqa: D102, N802
31
34
  user = base64.b64decode(self.headers["Authorization"].split(" ")[1]).decode().split(":")[0]
35
+ if user.startswith("slow-"):
36
+ time.sleep(SLOW_API_SECONDS)
32
37
  body = json.dumps({"categories": [user]}).encode()
33
38
  self.send_response(200)
34
39
  self.send_header("Content-Type", "application/json")
@@ -110,6 +115,23 @@ def test_concurrent_requests_use_their_own_token(http_server):
110
115
  assert bob.structuredContent["categories"] == ["bob-token"]
111
116
 
112
117
 
118
+ def test_a_slow_tool_does_not_hold_up_other_requests(http_server):
119
+ """A tool call still waiting on Code Ocean leaves the server free to serve another session."""
120
+ url, _ = http_server
121
+
122
+ async def call_and_time(token: str):
123
+ result = await _call_get_custom_metadata(url, token)
124
+ return result, time.monotonic()
125
+
126
+ async def both():
127
+ return await asyncio.gather(call_and_time("slow-token"), call_and_time("fast-token"))
128
+
129
+ (slow, slow_finished), (fast, fast_finished) = asyncio.run(both())
130
+ assert slow.structuredContent["categories"] == ["slow-token"]
131
+ assert fast.structuredContent["categories"] == ["fast-token"]
132
+ assert fast_finished < slow_finished - 1
133
+
134
+
113
135
  def test_request_without_credential_is_refused(http_server):
114
136
  """A request carrying no token is refused rather than served with the environment's client."""
115
137
  url, _ = http_server
@@ -558,9 +558,10 @@ wheels = [
558
558
 
559
559
  [[package]]
560
560
  name = "codeocean-mcp-server"
561
- version = "0.12.1"
561
+ version = "0.12.2"
562
562
  source = { editable = "." }
563
563
  dependencies = [
564
+ { name = "anyio" },
564
565
  { name = "codeocean" },
565
566
  { name = "mcp" },
566
567
  ]
@@ -577,6 +578,7 @@ dev = [
577
578
 
578
579
  [package.metadata]
579
580
  requires-dist = [
581
+ { name = "anyio", specifier = ">=4.5" },
580
582
  { name = "codeocean", specifier = ">=0.14.0,<0.15.0" },
581
583
  { name = "mcp", specifier = ">=1.23.0,<1.24.0" },
582
584
  ]