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.
Metadata
Release files for mcpsync-cli 0.1.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| mcpsync_cli-0.1.0.tar.gz | 32.3 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| mcpsync_cli-0.1.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 52.2 kB
Release files / mcpsync_cli-0.1.0.tar.gz
| Download URL | mcpsync_cli-0.1.0.tar.gz |
|---|---|
| Size | 32.3 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
754af2487d641d71d27f210b51f6536b02bdb3b6ea9de0cad47ca49a2f954bb6
|
|
BLAKE2b-256 checksum How to use checksums |
851ae1c1315b0530aca9c84b5af670e6439b224b95e376385936c6c3e444f64b
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/6.2.0 CPython/3.11.15
|
Release files / mcpsync_cli-0.1.0-py3-none-any.whl
| Download URL | mcpsync_cli-0.1.0-py3-none-any.whl |
|---|---|
| Size | 20.0 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
c582494f43963de359f3950bb18ed77b28d1ed5f1db4b09ed18b3aaaa411f992
|
|
BLAKE2b-256 checksum How to use checksums |
227a960ddbe69aca3274ec83dad011bc85b3cff57ca2b06f3d7fedf119af544a
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/6.2.0 CPython/3.11.15
|