mcpsync
Synchronous Python API for MCP servers — stdio and HTTP transports, plus a CLI for ad-hoc inspection.
mcpsync wraps the official mcp Python SDK's async client behind a
blocking SyncMCPClient so you can call MCP tools, list resources, and
read resources from synchronous code (CLI tools, Django views, Flask
handlers, scripts). It also ships a CLI for ad-hoc server inspection
from the shell.
Quick Start
Install from source:
pip install git+https://github.com/prasad-a-abhishek/mcpsync.git
Connect to a stdio MCP server and call a tool:
from mcpsync import SyncMCPClient, StdioServerParameters
params = StdioServerParameters(
command="python", args=("-m", "my_mcp_server"),
env={"DEBUG": "1"},
cwd="/srv/myapp",
)
with SyncMCPClient(params) as client:
tools = client.list_tools()
for tool in tools:
print(f"- {tool.name}: {tool.description}")
result = client.call_tool("add", {"a": 2, "b": 3})
for block in result.content:
print(block.text)
Connect to an HTTP MCP server:
from mcpsync import SyncMCPClient, HttpServerParameters
params = HttpServerParameters(
url="https://mcp.example.com/api",
headers={"Authorization": "Bearer ..."},
timeout=10.0,
)
with SyncMCPClient(params) as client:
resources = client.list_resources()
contents = client.read_resource(resources[0].uri)
Turn any async MCP helper into a sync function with the @sync
decorator:
from mcpsync import sync
@sync
async def load_schema(server_url: str) -> dict:
async with some_async_helper(server_url) as helper:
return await helper.fetch_schema()
⚡ Performance & Benchmarks
We publish benchmarks/BENCHMARK.md per Invariant 14 — including
the methodology caveat. The honest summary: mcpsync uses
asyncio.Runner per call, which actually benchmarks ~15% faster
than the SDK's asyncio.run pattern in this setup (interpreter
boot dominates; relative comparison is meaningful). Reproduce
locally:
python benchmarks/run_benchmark.py
The full results table, methodology, and the trade-off transparency statement are in benchmarks/BENCHMARK.md.
Why mcpsync? (Problem & Trade-Off Statement)
The MCP Python SDK ships an async-only client. Synchronous callers
(Django views, CLI tools, scripts) hit the same wall: every
asyncio.run() from sync code re-creates an event loop, leaks
resources, and breaks under asyncio.run() recursion if the
surrounding runtime already runs a loop.
Two GitHub issues document the gap:
modelcontextprotocol/python-sdk#1223— "Sync client API" (open, multiple reactions)modelcontextprotocol/python-sdk— the SDK explicitly documents theclient.session.stdiouse pattern as async only.
What mcpsync is: a thin wrapper that gives you SyncMCPClient +
a @sync decorator, plus a CLI for ad-hoc inspection. It uses the
official mcp SDK under the hood and never re-implements the JSON-RPC
protocol or transport.
What mcpsync is NOT:
- Not a replacement for the
mcpSDK — it depends on it. - Not an MCP server implementation. It's a client.
- Not magic. Each
SyncMCPClient.list_tools()call creates a one-shot event loop on a worker thread. If you need 100k calls/sec, use the SDK's async client directly.
Trade-offs you accept by using mcpsync:
- Each
SyncMCPClientcall creates a freshasyncio.Runneron a worker thread. The bench shows this is ~15% faster than the SDK baseline, but for a true long-lived async loop you should use the SDK's async client directly. - The close path needs a wall-clock fuse to escape a known deadlock
in the
mcpSDK'sstdio_client.__aexit__shielded cancel scope. Without the fuse the caller's process hangs. Seesrc/mcpsync/client.py::_run_boundedand the docstring onSyncMCPClient.close(). - HTTP transport requires the
mcpSDK's optionalhttpx2dep, which is re-exported bymcpfor convenience.
Key Features & Complete API / CLI Reference
Library API
| Name | Returns | Notes |
|---|---|---|
StdioServerParameters(command, args, env, cwd) |
dataclass | spawn the server as a subprocess |
HttpServerParameters(url, headers, timeout) |
dataclass | connect to a streamable-HTTP MCP server |
SyncMCPClient(params) |
context manager | open the session; use as with block |
client.list_tools() |
list[Tool] |
tool descriptors from mcp.types.Tool |
client.list_resources() |
list[Resource] |
resource descriptors from mcp.types.Resource |
client.call_tool(name, arguments) |
CallToolResult |
invoke a tool; result.content is a list of typed blocks |
client.read_resource(uri) |
ReadResourceResult |
fetch a resource by URI |
@sync decorator |
wraps an async function into a sync one | uses a per-call event loop |
MCPError |
exception | re-exported from mcp.shared.exceptions |
All public types are fully type-hinted. A py.typed marker ships in
src/mcpsync/ for PEP 561.
CLI
$ mcpsync --help
usage: mcpsync [-h] [--version] {stdio,http} ...
Synchronous client for MCP servers.
$ mcpsync stdio list-tools -- python -m my_server
[
{"name": "echo", "description": "Echo the input message back", "inputSchema": {...}},
...
]
$ mcpsync stdio call-tool add '{"a":2,"b":3}' -- python -m my_server
{"content":[{"type":"text","text":"5"}]}
$ mcpsync stdio list-resources -- python -m my_server
[{"uri": "file:///greeting.txt", "name": "greeting", "mimeType": "text/plain"}, ...]
$ mcpsync stdio call-resource 'file:///greeting.txt' -- python -m my_server
{"contents":[{"uri": "file:///greeting.txt", "text": "Hello!", "mimeType": "text/plain"}]}
$ mcpsync http list-tools --url https://mcp.example.com/api
$ mcpsync http call-tool add '{"a":2}' --url https://mcp.example.com/api
The stdio subcommand takes a -- separator before the server
command + args. The http subcommand takes --url, --header (repeatable
KEY=VAL), and --timeout.
Out of scope
- MCP server implementation
- Long-lived event-loop reuse across calls (each call creates a new one; see trade-off above)
- Streaming / subscription primitives
- Server-side protocol:
mcpsyncis a client only - Any protocol version below what the bundled
mcpSDK supports
Limitations
- One shot per call. Each
SyncMCPClientuse opens the server process, runs the request, and closes. If you need a long-lived connection, keep thewithblock open and make many calls inside. - HTTP transport requires the
httpx2runtime dep (re-exported bymcpfor convenience). - Close path is bounded by a wall-clock fuse to avoid a known
deadlock in the
mcpSDK's stdio teardown.
Tests
pytest tests/ -q
129 tests across 1 file (split into 16 test classes by behavior).
Coverage spans every spec acceptance criterion plus angular sweep
(empty/None inputs, unicode, large payloads, malformed JSON, CLI
end-to-end, concurrent @sync calls, parameter adversarial
inputs, and regression guards for the close-path deadlock).
License
MIT — see LICENSE.
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file mcpsync_cli-0.1.0.tar.gz.
File metadata
- Download URL: mcpsync_cli-0.1.0.tar.gz
- Upload date:
- Size: 32.3 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via:
twine/6.2.0 CPython/3.11.15
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
754af2487d641d71d27f210b51f6536b02bdb3b6ea9de0cad47ca49a2f954bb6
|
|
| MD5 |
b9d09f52dc2822188c47e46d6b55a07d
|
|
| BLAKE2b-256 |
851ae1c1315b0530aca9c84b5af670e6439b224b95e376385936c6c3e444f64b
|
File details
Details for the file mcpsync_cli-0.1.0-py3-none-any.whl.
File metadata
- Download URL: mcpsync_cli-0.1.0-py3-none-any.whl
- Upload date:
- Size: 20.0 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via:
twine/6.2.0 CPython/3.11.15
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
c582494f43963de359f3950bb18ed77b28d1ed5f1db4b09ed18b3aaaa411f992
|
|
| MD5 |
7c0debe798cff6a444649e398ea12937
|
|
| BLAKE2b-256 |
227a960ddbe69aca3274ec83dad011bc85b3cff57ca2b06f3d7fedf119af544a
|