codeocean-mcp-server 0.12.1__tar.gz → 0.13.0__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.
- {codeocean_mcp_server-0.12.1 → codeocean_mcp_server-0.13.0}/CHANGELOG.md +11 -0
- {codeocean_mcp_server-0.12.1 → codeocean_mcp_server-0.13.0}/PKG-INFO +5 -4
- {codeocean_mcp_server-0.12.1 → codeocean_mcp_server-0.13.0}/README.md +1 -1
- {codeocean_mcp_server-0.12.1 → codeocean_mcp_server-0.13.0}/pyproject.toml +5 -3
- {codeocean_mcp_server-0.12.1 → codeocean_mcp_server-0.13.0}/src/codeocean_mcp_server/server.py +33 -1
- {codeocean_mcp_server-0.12.1 → codeocean_mcp_server-0.13.0}/src/codeocean_mcp_server/tools/capsules.py +47 -1
- {codeocean_mcp_server-0.12.1 → codeocean_mcp_server-0.13.0}/src/codeocean_mcp_server/tools/data_assets.py +6 -0
- {codeocean_mcp_server-0.12.1 → codeocean_mcp_server-0.13.0}/tests/test_http_transport.py +57 -7
- codeocean_mcp_server-0.13.0/uv.lock +3842 -0
- codeocean_mcp_server-0.12.1/uv.lock +0 -3272
- {codeocean_mcp_server-0.12.1 → codeocean_mcp_server-0.13.0}/.gitignore +0 -0
- {codeocean_mcp_server-0.12.1 → codeocean_mcp_server-0.13.0}/.python-version +0 -0
- {codeocean_mcp_server-0.12.1 → codeocean_mcp_server-0.13.0}/LICENSE +0 -0
- {codeocean_mcp_server-0.12.1 → codeocean_mcp_server-0.13.0}/RELEASE.md +0 -0
- {codeocean_mcp_server-0.12.1 → codeocean_mcp_server-0.13.0}/src/codeocean_mcp_server/__init__.py +0 -0
- {codeocean_mcp_server-0.12.1 → codeocean_mcp_server-0.13.0}/src/codeocean_mcp_server/client.py +0 -0
- {codeocean_mcp_server-0.12.1 → codeocean_mcp_server-0.13.0}/src/codeocean_mcp_server/file_utils.py +0 -0
- {codeocean_mcp_server-0.12.1 → codeocean_mcp_server-0.13.0}/src/codeocean_mcp_server/logging_config.py +0 -0
- {codeocean_mcp_server-0.12.1 → codeocean_mcp_server-0.13.0}/src/codeocean_mcp_server/models.py +0 -0
- {codeocean_mcp_server-0.12.1 → codeocean_mcp_server-0.13.0}/src/codeocean_mcp_server/search.py +0 -0
- {codeocean_mcp_server-0.12.1 → codeocean_mcp_server-0.13.0}/src/codeocean_mcp_server/tools/__init__.py +0 -0
- {codeocean_mcp_server-0.12.1 → codeocean_mcp_server-0.13.0}/src/codeocean_mcp_server/tools/computations.py +0 -0
- {codeocean_mcp_server-0.12.1 → codeocean_mcp_server-0.13.0}/src/codeocean_mcp_server/tools/custom_metadata.py +0 -0
- {codeocean_mcp_server-0.12.1 → codeocean_mcp_server-0.13.0}/tests/bedrock_call.py +0 -0
- {codeocean_mcp_server-0.12.1 → codeocean_mcp_server-0.13.0}/tests/bedrock_tools_converter.py +0 -0
- {codeocean_mcp_server-0.12.1 → codeocean_mcp_server-0.13.0}/tests/conftest.py +0 -0
- {codeocean_mcp_server-0.12.1 → codeocean_mcp_server-0.13.0}/tests/mcp_client.py +0 -0
- {codeocean_mcp_server-0.12.1 → codeocean_mcp_server-0.13.0}/tests/test_logging_config.py +0 -0
- {codeocean_mcp_server-0.12.1 → codeocean_mcp_server-0.13.0}/tests/test_multi_tool_calling.py +0 -0
- {codeocean_mcp_server-0.12.1 → codeocean_mcp_server-0.13.0}/tests/test_search.py +0 -0
- {codeocean_mcp_server-0.12.1 → codeocean_mcp_server-0.13.0}/tests/test_tool_calling.py +0 -0
|
@@ -1,6 +1,17 @@
|
|
|
1
1
|
CHANGELOG
|
|
2
2
|
=========
|
|
3
3
|
|
|
4
|
+
## 0.13.0 (2026-09-16)
|
|
5
|
+
|
|
6
|
+
- [#46](https://github.com/codeocean/codeocean-mcp-server/pull/46) fix: bump mcp to 1.29.x and refresh locked dependencies
|
|
7
|
+
- [#37](https://github.com/codeocean/codeocean-mcp-server/pull/37) feat: add capsule and pipeline release tools
|
|
8
|
+
- [#47](https://github.com/codeocean/codeocean-mcp-server/pull/47) feat: add permissions and Git sync tools
|
|
9
|
+
- **Minimum Code Ocean platform version updated to `4.8.0`.**
|
|
10
|
+
|
|
11
|
+
## 0.12.2 (2026-09-03)
|
|
12
|
+
|
|
13
|
+
- [#42](https://github.com/codeocean/codeocean-mcp-server/pull/42) fix: run synchronous tool bodies in a worker thread
|
|
14
|
+
|
|
4
15
|
## 0.12.1 (2026-08-28)
|
|
5
16
|
|
|
6
17
|
- [#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.
|
|
3
|
+
Version: 0.13.0
|
|
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,15 +12,16 @@ 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:
|
|
16
|
-
Requires-Dist:
|
|
15
|
+
Requires-Dist: anyio>=4.5
|
|
16
|
+
Requires-Dist: codeocean<0.18.0,>=0.17.0
|
|
17
|
+
Requires-Dist: mcp<1.30.0,>=1.29.1
|
|
17
18
|
Description-Content-Type: text/markdown
|
|
18
19
|
|
|
19
20
|
# Code Ocean MCP Server
|
|
20
21
|
|
|
21
22
|
Model Context Protocol (MCP) server for Code Ocean.
|
|
22
23
|
|
|
23
|
-
This MCP server provides tools to search and
|
|
24
|
+
This MCP server provides tools to search, run, and release capsules and pipelines, and manage data assets.
|
|
24
25
|
|
|
25
26
|
## Table of Contents
|
|
26
27
|
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
Model Context Protocol (MCP) server for Code Ocean.
|
|
4
4
|
|
|
5
|
-
This MCP server provides tools to search and
|
|
5
|
+
This MCP server provides tools to search, run, and release capsules and pipelines, and manage data assets.
|
|
6
6
|
|
|
7
7
|
## Table of Contents
|
|
8
8
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "codeocean-mcp-server"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.13.0"
|
|
4
4
|
authors = [{ name = "Code Ocean", email = "dev@codeocean.com" }]
|
|
5
5
|
description = "Code Ocean MCP Server"
|
|
6
6
|
readme = "README.md"
|
|
@@ -11,8 +11,9 @@ classifiers = [
|
|
|
11
11
|
"Operating System :: OS Independent",
|
|
12
12
|
]
|
|
13
13
|
dependencies = [
|
|
14
|
-
"
|
|
15
|
-
"
|
|
14
|
+
"anyio>=4.5",
|
|
15
|
+
"codeocean>=0.17.0,<0.18.0",
|
|
16
|
+
"mcp>=1.29.1,<1.30.0",
|
|
16
17
|
]
|
|
17
18
|
license = "MIT"
|
|
18
19
|
|
|
@@ -21,6 +22,7 @@ dev = [
|
|
|
21
22
|
"boto3>=1.42.17",
|
|
22
23
|
"deepdiff>=8.6.1",
|
|
23
24
|
"hatch>=1.16.2",
|
|
25
|
+
"httpx>=0.28.1",
|
|
24
26
|
"mcp-python-client>=0.1.9",
|
|
25
27
|
"pytest>=9.0.2",
|
|
26
28
|
"ruff>=0.14.10",
|
{codeocean_mcp_server-0.12.1 → codeocean_mcp_server-0.13.0}/src/codeocean_mcp_server/server.py
RENAMED
|
@@ -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 =
|
|
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}."
|
|
@@ -2,11 +2,14 @@ from codeocean import CodeOcean
|
|
|
2
2
|
from codeocean.capsule import (
|
|
3
3
|
AppPanel,
|
|
4
4
|
Capsule,
|
|
5
|
+
CapsuleReleaseJob,
|
|
5
6
|
CapsuleSearchParams,
|
|
6
7
|
Computation,
|
|
7
8
|
DataAssetAttachParams,
|
|
8
9
|
DataAssetAttachResults,
|
|
10
|
+
GitSyncResults,
|
|
9
11
|
)
|
|
12
|
+
from codeocean.components import Permissions
|
|
10
13
|
from mcp.server.fastmcp import FastMCP
|
|
11
14
|
|
|
12
15
|
from codeocean_mcp_server.models import dataclass_to_pydantic
|
|
@@ -17,7 +20,7 @@ CapsuleSearchParamsModel = dataclass_to_pydantic(CapsuleSearchParams)
|
|
|
17
20
|
DataAssetAttachParamsModel = dataclass_to_pydantic(DataAssetAttachParams)
|
|
18
21
|
|
|
19
22
|
|
|
20
|
-
def add_tools(mcp: FastMCP, client: CodeOcean):
|
|
23
|
+
def add_tools(mcp: FastMCP, client: CodeOcean): # noqa: C901
|
|
21
24
|
"""Add capsule tools to the MCP server."""
|
|
22
25
|
|
|
23
26
|
@mcp.tool(description=(str(client.capsules.search_capsules.__doc__) + " " + str(CapsuleSearchResults.__doc__)))
|
|
@@ -86,3 +89,46 @@ def add_tools(mcp: FastMCP, client: CodeOcean):
|
|
|
86
89
|
def get_capsule_app_panel(capsule_id: str, version: int | None = None) -> AppPanelModel:
|
|
87
90
|
"""Retrieve the app panel for a capsule, optionally for a specific version."""
|
|
88
91
|
return client.capsules.get_capsule_app_panel(capsule_id, version)
|
|
92
|
+
|
|
93
|
+
@mcp.tool(description=(str(client.capsules.get_permissions.__doc__) + " Accepts a capsule ID or a pipeline ID."))
|
|
94
|
+
def get_capsule_permissions(capsule_id: str) -> Permissions:
|
|
95
|
+
"""Get the users, groups and everyone-role permissions of a capsule or pipeline."""
|
|
96
|
+
return client.capsules.get_permissions(capsule_id)
|
|
97
|
+
|
|
98
|
+
@mcp.tool(
|
|
99
|
+
description=(
|
|
100
|
+
str(client.capsules.sync_capsule.__doc__)
|
|
101
|
+
+ " Accepts a capsule ID or a pipeline ID. This pushes and pulls commits against the external"
|
|
102
|
+
" Git remote, so call it only when the user asked to sync."
|
|
103
|
+
)
|
|
104
|
+
)
|
|
105
|
+
def sync_capsule(capsule_id: str) -> GitSyncResults:
|
|
106
|
+
"""Sync a capsule or pipeline with its linked external Git repository."""
|
|
107
|
+
return client.capsules.sync_capsule(capsule_id)
|
|
108
|
+
|
|
109
|
+
@mcp.tool(description=(str(client.capsules.release_capsule.__doc__) + " Accepts a capsule ID or a pipeline ID."))
|
|
110
|
+
def release_capsule(capsule_id: str) -> CapsuleReleaseJob:
|
|
111
|
+
"""Start releasing a new version of an already-released capsule or pipeline."""
|
|
112
|
+
return client.capsules.release_capsule(capsule_id)
|
|
113
|
+
|
|
114
|
+
@mcp.tool(description=(str(client.capsules.get_release_job.__doc__) + " Accepts a capsule ID or a pipeline ID."))
|
|
115
|
+
def get_release_job(capsule_id: str, job_id: str) -> CapsuleReleaseJob:
|
|
116
|
+
"""Get the status of a capsule or pipeline release job."""
|
|
117
|
+
return client.capsules.get_release_job(capsule_id, job_id)
|
|
118
|
+
|
|
119
|
+
@mcp.tool(
|
|
120
|
+
description=(
|
|
121
|
+
str(client.capsules.wait_until_release_completed.__doc__)
|
|
122
|
+
+ " Accepts a capsule ID or a pipeline ID. Set `timeout` to bound the wait - without one this"
|
|
123
|
+
" polls until the release reaches a terminal state, however long that takes."
|
|
124
|
+
)
|
|
125
|
+
)
|
|
126
|
+
def wait_until_release_completed(
|
|
127
|
+
capsule_id: str,
|
|
128
|
+
job_id: str,
|
|
129
|
+
polling_interval: float = 5,
|
|
130
|
+
timeout: float | None = None,
|
|
131
|
+
) -> CapsuleReleaseJob:
|
|
132
|
+
"""Wait until a capsule or pipeline release job reaches a terminal state."""
|
|
133
|
+
job = client.capsules.get_release_job(capsule_id, job_id)
|
|
134
|
+
return client.capsules.wait_until_release_completed(capsule_id, job, polling_interval, timeout)
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import os
|
|
2
2
|
|
|
3
3
|
from codeocean import CodeOcean
|
|
4
|
+
from codeocean.components import Permissions
|
|
4
5
|
from codeocean.data_asset import (
|
|
5
6
|
DataAsset,
|
|
6
7
|
DataAssetParams,
|
|
@@ -62,6 +63,11 @@ def add_tools(mcp: FastMCP, client: CodeOcean):
|
|
|
62
63
|
file_urls = client.data_assets.get_data_asset_file_urls(data_asset_id, file_path)
|
|
63
64
|
return download_and_read_file(file_urls.download_url)
|
|
64
65
|
|
|
66
|
+
@mcp.tool(description=client.data_assets.get_permissions.__doc__)
|
|
67
|
+
def get_data_asset_permissions(data_asset_id: str) -> Permissions:
|
|
68
|
+
"""Get the users, groups and everyone-role permissions of a data asset."""
|
|
69
|
+
return client.data_assets.get_permissions(data_asset_id)
|
|
70
|
+
|
|
65
71
|
@mcp.tool(description=client.data_assets.list_data_asset_files.__doc__)
|
|
66
72
|
def list_data_asset_files(data_asset_id: str, path: str = "") -> Folder:
|
|
67
73
|
"""List files in a data asset."""
|
|
@@ -16,19 +16,40 @@ import time
|
|
|
16
16
|
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
|
|
17
17
|
from pathlib import Path
|
|
18
18
|
|
|
19
|
+
import httpx
|
|
19
20
|
import pytest
|
|
20
21
|
from mcp import ClientSession, StdioServerParameters
|
|
21
|
-
from mcp.client.streamable_http import
|
|
22
|
+
from mcp.client.streamable_http import streamable_http_client
|
|
22
23
|
from mcp_client import get_tools
|
|
23
24
|
|
|
24
25
|
SERVER_SCRIPT_PATH = str(Path(__file__).parent.parent / "src" / "codeocean_mcp_server" / "server.py")
|
|
25
26
|
|
|
27
|
+
# Upper bound on the test's own waits, so a regression fails the test instead of hanging it.
|
|
28
|
+
RENDEZVOUS_TIMEOUT = 5.0
|
|
29
|
+
|
|
30
|
+
# The stub API's escape hatch from a release that never comes, far enough past the test's own waits
|
|
31
|
+
# that it cannot be what frees the server first.
|
|
32
|
+
HELD_REQUEST_TIMEOUT = 60.0
|
|
33
|
+
|
|
26
34
|
|
|
27
35
|
class _EchoTokenHandler(BaseHTTPRequestHandler):
|
|
28
|
-
"""Answer the custom metadata endpoint with the basic-auth user, which is the API token.
|
|
36
|
+
"""Answer the custom metadata endpoint with the basic-auth user, which is the API token.
|
|
37
|
+
|
|
38
|
+
A token named "slow-..." stands in for a tool that waits on Code Ocean: the handler reports that
|
|
39
|
+
the request has arrived, then holds the response until the test releases it. Only a released
|
|
40
|
+
request is answered, so the test cannot mistake a lapsed hold for a server that stayed free.
|
|
41
|
+
"""
|
|
42
|
+
|
|
43
|
+
slow_started = threading.Event()
|
|
44
|
+
slow_released = threading.Event()
|
|
29
45
|
|
|
30
46
|
def do_GET(self): # noqa: D102, N802
|
|
31
47
|
user = base64.b64decode(self.headers["Authorization"].split(" ")[1]).decode().split(":")[0]
|
|
48
|
+
if user.startswith("slow-"):
|
|
49
|
+
self.slow_started.set()
|
|
50
|
+
if not self.slow_released.wait(HELD_REQUEST_TIMEOUT):
|
|
51
|
+
self.send_error(500, "the held request was never released")
|
|
52
|
+
return
|
|
32
53
|
body = json.dumps({"categories": [user]}).encode()
|
|
33
54
|
self.send_response(200)
|
|
34
55
|
self.send_header("Content-Type", "application/json")
|
|
@@ -89,10 +110,11 @@ def _stdio_server(domain: str) -> StdioServerParameters:
|
|
|
89
110
|
|
|
90
111
|
async def _call_get_custom_metadata(url: str, token: str | None, scheme: str = "Bearer"):
|
|
91
112
|
headers = {"Authorization": f"{scheme} {token}"} if token else None
|
|
92
|
-
async with
|
|
93
|
-
async with
|
|
94
|
-
|
|
95
|
-
|
|
113
|
+
async with httpx.AsyncClient(headers=headers) as http_client:
|
|
114
|
+
async with streamable_http_client(url, http_client=http_client) as (read_stream, write_stream, _):
|
|
115
|
+
async with ClientSession(read_stream, write_stream) as session:
|
|
116
|
+
await session.initialize()
|
|
117
|
+
return await session.call_tool("get_custom_metadata", {})
|
|
96
118
|
|
|
97
119
|
|
|
98
120
|
def test_concurrent_requests_use_their_own_token(http_server):
|
|
@@ -110,6 +132,34 @@ def test_concurrent_requests_use_their_own_token(http_server):
|
|
|
110
132
|
assert bob.structuredContent["categories"] == ["bob-token"]
|
|
111
133
|
|
|
112
134
|
|
|
135
|
+
def test_a_slow_tool_does_not_hold_up_other_requests(http_server):
|
|
136
|
+
"""A second session is served while a tool call is still waiting on Code Ocean.
|
|
137
|
+
|
|
138
|
+
Fails if the synchronous tool runs on the event loop: the fast request then cannot even
|
|
139
|
+
initialize until the slow one is released, and times out.
|
|
140
|
+
"""
|
|
141
|
+
url, _ = http_server
|
|
142
|
+
handler = _EchoTokenHandler
|
|
143
|
+
|
|
144
|
+
async def fast_while_slow_is_blocked():
|
|
145
|
+
slow = asyncio.create_task(_call_get_custom_metadata(url, "slow-token"))
|
|
146
|
+
try:
|
|
147
|
+
assert await asyncio.to_thread(handler.slow_started.wait, RENDEZVOUS_TIMEOUT), (
|
|
148
|
+
"the slow tool call never reached the stub API"
|
|
149
|
+
)
|
|
150
|
+
try:
|
|
151
|
+
fast = await asyncio.wait_for(_call_get_custom_metadata(url, "fast-token"), RENDEZVOUS_TIMEOUT)
|
|
152
|
+
except asyncio.TimeoutError:
|
|
153
|
+
pytest.fail("the fast request did not complete while the slow tool call was blocked")
|
|
154
|
+
finally:
|
|
155
|
+
handler.slow_released.set()
|
|
156
|
+
return await slow, fast
|
|
157
|
+
|
|
158
|
+
slow, fast = asyncio.run(fast_while_slow_is_blocked())
|
|
159
|
+
assert slow.structuredContent["categories"] == ["slow-token"]
|
|
160
|
+
assert fast.structuredContent["categories"] == ["fast-token"]
|
|
161
|
+
|
|
162
|
+
|
|
113
163
|
def test_request_without_credential_is_refused(http_server):
|
|
114
164
|
"""A request carrying no token is refused rather than served with the environment's client."""
|
|
115
165
|
url, _ = http_server
|
|
@@ -131,7 +181,7 @@ def test_tool_definitions_match_stdio(http_server):
|
|
|
131
181
|
url, domain = http_server
|
|
132
182
|
|
|
133
183
|
async def list_over_http():
|
|
134
|
-
async with
|
|
184
|
+
async with streamable_http_client(url) as (read_stream, write_stream, _):
|
|
135
185
|
async with ClientSession(read_stream, write_stream) as session:
|
|
136
186
|
await session.initialize()
|
|
137
187
|
return (await session.list_tools()).tools
|