Metadata-Version: 2.4
Name: mcpsync-cli
Version: 0.1.0
Summary: Synchronous MCP Python client — call MCP servers from sync Python without async/await
Author-email: Repo Factory <noreply@example.com>
License: MIT
Project-URL: Homepage, https://github.com/prasad-a-abhishek/mcpsync
Project-URL: Repository, https://github.com/prasad-a-abhishek/mcpsync
Project-URL: Issues, https://github.com/prasad-a-abhishek/mcpsync/issues
Keywords: mcp,model-context-protocol,synchronous,sync,client,stdio,http,streamable-http
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Classifier: Typing :: Typed
Requires-Python: >=3.11
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: mcp>=1.0
Provides-Extra: dev
Requires-Dist: pytest>=7.0; extra == "dev"
Dynamic: license-file

# mcpsync

[![PyPI](https://img.shields.io/badge/version-0.1.0-blue)](https://pypi.org/project/mcpsync)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)
[![Python 3.11+](https://img.shields.io/badge/python-3.11+-blue.svg)](https://www.python.org/)
[![Tests](https://img.shields.io/badge/tests-129%20passing-brightgreen.svg)](tests/)
[![Min runtime deps](https://img.shields.io/badge/dependencies-mcp-blue)](pyproject.toml)

> **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:

```bash
pip install git+https://github.com/prasad-a-abhishek/mcpsync.git
```

Connect to a stdio MCP server and call a tool:

```python
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:

```python
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:

```python
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:

```bash
python benchmarks/run_benchmark.py
```

The full results table, methodology, and the trade-off
transparency statement are in
[benchmarks/BENCHMARK.md](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`](https://github.com/modelcontextprotocol/python-sdk/issues/1223) — "Sync client API" (open, multiple reactions)
- `modelcontextprotocol/python-sdk` — the SDK explicitly documents
  the `client.session.stdio` use 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 `mcp` SDK — 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 `SyncMCPClient` call creates a fresh `asyncio.Runner` on 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 `mcp` SDK's `stdio_client.__aexit__` shielded cancel scope.
  Without the fuse the caller's process hangs. See
  `src/mcpsync/client.py::_run_bounded` and the docstring on
  `SyncMCPClient.close()`.
- HTTP transport requires the `mcp` SDK's optional `httpx2` dep,
  which is re-exported by `mcp` for 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

```text
$ 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: `mcpsync` is a client only
- Any protocol version below what the bundled `mcp` SDK supports

### Limitations

- **One shot per call.** Each `SyncMCPClient` use opens the server
  process, runs the request, and closes. If you need a long-lived
  connection, keep the `with` block open and make many calls inside.
- **HTTP transport** requires the `httpx2` runtime dep (re-exported
  by `mcp` for convenience).
- **Close path** is bounded by a wall-clock fuse to avoid a known
  deadlock in the `mcp` SDK's stdio teardown.

## Tests

```bash
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](LICENSE).
