-
Notifications
You must be signed in to change notification settings - Fork 4k
Split the client SDK into a standalone mcp-client package #3583
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
e49a483
7db42b6
eed6aaa
b16ce08
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -48,6 +48,38 @@ jobs: | |
| uv run --isolated --no-project --with ./src/mcp-types python -c \ | ||
| "import mcp_types, mcp_types.jsonrpc, mcp_types.methods, mcp_types.version, mcp_types._v2025_11_25, mcp_types._v2026_07_28" | ||
|
|
||
| packages: | ||
| runs-on: ubuntu-latest | ||
| steps: | ||
| - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 | ||
| with: | ||
| persist-credentials: false | ||
| - uses: astral-sh/setup-uv@20cfd1bf945f4377ade1205e4dbc17946fc9a30d # v10.0.1 | ||
| with: | ||
| version: 0.9.5 | ||
| - name: Build all distributions | ||
| run: | | ||
| uv build --package mcp-types | ||
| uv build --package mcp-client | ||
| uv build --package mcp | ||
| - name: Exercise the client wheel and sdist without the server SDK | ||
| run: | | ||
| for package in dist/mcp_client-*.whl dist/mcp_client-*.tar.gz; do | ||
| uv run --isolated --no-project --find-links dist --with "$package" \ | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. P2: The package checks run under only the runner's implicit Python version, so they do not validate the built wheel and sdist at the supported 3.10 and 3.14 endpoints. Add a Python matrix or explicit Prompt for AI agents |
||
| python scripts/check_client_package.py | ||
| done | ||
| - name: Type-check the standalone client package | ||
| run: | | ||
| uv export --frozen --package mcp-client --no-default-groups --no-dev --no-emit-workspace \ | ||
| --output-file "$RUNNER_TEMP/client-requirements.txt" | ||
| uv run --isolated --no-project --find-links dist --with dist/mcp_client-*.whl \ | ||
| --with-requirements "$RUNNER_TEMP/client-requirements.txt" --with pyright==1.1.405 \ | ||
| python scripts/check_client_types.py | ||
| - name: Import the full SDK with the client package first | ||
| run: | | ||
| uv run --isolated --no-project --find-links dist --with dist/mcp-*.whl python -c \ | ||
| 'import mcp_client, mcp; from typing import get_type_hints; assert mcp.Client is mcp_client.Client; get_type_hints(mcp.Client)' | ||
|
|
||
| test: | ||
| name: test (${{ matrix.python-version }}, ${{ matrix.dep-resolution.name }}, ${{ matrix.os }}) | ||
| runs-on: ${{ matrix.os }} | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,71 @@ | ||
| """Exercise an installed client distribution without the SDK's server dependencies. | ||
|
|
||
| The peer uses raw messages because the server package must be absent from this environment. | ||
| """ | ||
|
|
||
| import importlib | ||
| import importlib.util | ||
| import pkgutil | ||
| from collections.abc import AsyncIterator | ||
| from contextlib import asynccontextmanager | ||
| from typing import get_type_hints | ||
|
|
||
| import anyio | ||
| import mcp_client | ||
| from mcp_client.shared import exceptions | ||
| from mcp_client.shared.memory import MessageStream, create_client_server_memory_streams | ||
| from mcp_client.shared.message import SessionMessage | ||
| from mcp_types import JSONRPCRequest, JSONRPCResponse, ListToolsResult, Tool | ||
|
|
||
| get_type_hints(mcp_client.Client.__init__) | ||
|
|
||
| for error_type in ( | ||
| exceptions.MCPError, | ||
| exceptions.MCPDeprecationWarning, | ||
| exceptions.NoBackChannelError, | ||
| exceptions.UrlElicitationRequiredError, | ||
| ): | ||
| assert error_type.__module__ == "mcp_client.shared.exceptions" | ||
|
|
||
| for name in ("mcp", "starlette", "uvicorn", "sse_starlette", "multipart"): | ||
| assert importlib.util.find_spec(name) is None, name | ||
|
|
||
| for info in pkgutil.walk_packages(mcp_client.__path__, prefix="mcp_client."): | ||
| if not any(part.startswith("_") for part in info.name.split(".")): | ||
| importlib.import_module(info.name) | ||
|
|
||
| RESULT = ListToolsResult(tools=[Tool(name="example", input_schema={"type": "object"})]) | ||
|
|
||
|
|
||
| @asynccontextmanager | ||
| async def transport() -> AsyncIterator[MessageStream]: | ||
| async with create_client_server_memory_streams() as (client_streams, server_streams): | ||
| read, write = server_streams | ||
|
|
||
| async def respond() -> None: | ||
| received = await read.receive() | ||
| assert isinstance(received, SessionMessage) | ||
| message = received.message | ||
| assert isinstance(message, JSONRPCRequest) | ||
| assert message.method == "tools/list" | ||
| await write.send( | ||
| SessionMessage( | ||
| JSONRPCResponse( | ||
| jsonrpc="2.0", id=message.id, result=RESULT.model_dump(by_alias=True, exclude_none=True) | ||
| ) | ||
| ) | ||
| ) | ||
|
|
||
| async with anyio.create_task_group() as group: | ||
| group.start_soon(respond) | ||
| yield client_streams | ||
|
|
||
|
|
||
| async def main() -> None: | ||
| """Verify a client request and response through the installed public API.""" | ||
| with anyio.fail_after(5): | ||
| async with mcp_client.Client(transport(), mode="2026-07-28") as client: | ||
| assert await client.list_tools() == RESULT | ||
|
|
||
|
|
||
| anyio.run(main) |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,48 @@ | ||
| """Type-check the installed client package and reject an invalid constructor argument.""" | ||
|
|
||
| import importlib.util | ||
| import json | ||
| import subprocess | ||
| import sys | ||
| import tempfile | ||
| from pathlib import Path | ||
|
|
||
| import mcp_client | ||
|
|
||
| assert importlib.util.find_spec("mcp") is None | ||
|
|
||
| with tempfile.TemporaryDirectory() as directory: | ||
| root = Path(directory) | ||
| consumer = root / "consumer.py" | ||
| consumer.write_text( | ||
| "from mcp_client import Client, StdioServerParameters\n" | ||
| 'Client("https://example.com/mcp")\n' | ||
| 'Client(StdioServerParameters(command="python"))\n' | ||
| "Client(42)\n", | ||
| encoding="utf-8", | ||
| ) | ||
| config = root / "pyrightconfig.json" | ||
| config.write_text( | ||
| json.dumps( | ||
| { | ||
| "pythonPath": sys.executable, | ||
| "typeCheckingMode": "strict", | ||
| } | ||
| ), | ||
| encoding="utf-8", | ||
| ) | ||
| result = subprocess.run( | ||
| ["pyright", "--project", str(config), "--outputjson", *mcp_client.__path__, str(consumer)], | ||
| cwd=root, | ||
| capture_output=True, | ||
| text=True, | ||
| encoding="utf-8", | ||
| check=False, | ||
| ) | ||
| diagnostics = json.loads(result.stdout)["generalDiagnostics"] | ||
| errors = [diagnostic for diagnostic in diagnostics if diagnostic["severity"] == "error"] | ||
| assert result.returncode == 1, result.stdout + result.stderr | ||
| assert len(errors) == 1, result.stdout | ||
| assert errors[0]["file"] == str(consumer), result.stdout | ||
| assert errors[0]["range"]["start"]["line"] == 3, result.stdout | ||
| assert errors[0]["rule"] == "reportArgumentType", result.stdout |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why do we have this job?