mcpsync-cli 0.1.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.
- mcpsync_cli-0.1.0/LICENSE +21 -0
- mcpsync_cli-0.1.0/PKG-INFO +240 -0
- mcpsync_cli-0.1.0/README.md +213 -0
- mcpsync_cli-0.1.0/pyproject.toml +63 -0
- mcpsync_cli-0.1.0/setup.cfg +4 -0
- mcpsync_cli-0.1.0/src/mcpsync/__init__.py +31 -0
- mcpsync_cli-0.1.0/src/mcpsync/__init__.pyi +64 -0
- mcpsync_cli-0.1.0/src/mcpsync/__main__.py +5 -0
- mcpsync_cli-0.1.0/src/mcpsync/cli.py +330 -0
- mcpsync_cli-0.1.0/src/mcpsync/client.py +437 -0
- mcpsync_cli-0.1.0/src/mcpsync/errors.py +13 -0
- mcpsync_cli-0.1.0/src/mcpsync/params.py +110 -0
- mcpsync_cli-0.1.0/src/mcpsync/py.typed +0 -0
- mcpsync_cli-0.1.0/src/mcpsync/sync.py +61 -0
- mcpsync_cli-0.1.0/src/mcpsync_cli.egg-info/PKG-INFO +240 -0
- mcpsync_cli-0.1.0/src/mcpsync_cli.egg-info/SOURCES.txt +19 -0
- mcpsync_cli-0.1.0/src/mcpsync_cli.egg-info/dependency_links.txt +1 -0
- mcpsync_cli-0.1.0/src/mcpsync_cli.egg-info/entry_points.txt +2 -0
- mcpsync_cli-0.1.0/src/mcpsync_cli.egg-info/requires.txt +4 -0
- mcpsync_cli-0.1.0/src/mcpsync_cli.egg-info/top_level.txt +1 -0
- mcpsync_cli-0.1.0/tests/test_mcpsync.py +1590 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Repo Factory
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: mcpsync-cli
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Synchronous MCP Python client — call MCP servers from sync Python without async/await
|
|
5
|
+
Author-email: Repo Factory <noreply@example.com>
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/prasad-a-abhishek/mcpsync
|
|
8
|
+
Project-URL: Repository, https://github.com/prasad-a-abhishek/mcpsync
|
|
9
|
+
Project-URL: Issues, https://github.com/prasad-a-abhishek/mcpsync/issues
|
|
10
|
+
Keywords: mcp,model-context-protocol,synchronous,sync,client,stdio,http,streamable-http
|
|
11
|
+
Classifier: Development Status :: 4 - Beta
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
14
|
+
Classifier: Operating System :: OS Independent
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
18
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
19
|
+
Classifier: Typing :: Typed
|
|
20
|
+
Requires-Python: >=3.11
|
|
21
|
+
Description-Content-Type: text/markdown
|
|
22
|
+
License-File: LICENSE
|
|
23
|
+
Requires-Dist: mcp>=1.0
|
|
24
|
+
Provides-Extra: dev
|
|
25
|
+
Requires-Dist: pytest>=7.0; extra == "dev"
|
|
26
|
+
Dynamic: license-file
|
|
27
|
+
|
|
28
|
+
# mcpsync
|
|
29
|
+
|
|
30
|
+
[](https://pypi.org/project/mcpsync)
|
|
31
|
+
[](LICENSE)
|
|
32
|
+
[](https://www.python.org/)
|
|
33
|
+
[](tests/)
|
|
34
|
+
[](pyproject.toml)
|
|
35
|
+
|
|
36
|
+
> **Synchronous Python API for MCP servers — stdio and HTTP transports, plus a CLI for ad-hoc inspection.**
|
|
37
|
+
|
|
38
|
+
`mcpsync` wraps the official `mcp` Python SDK's async client behind a
|
|
39
|
+
blocking `SyncMCPClient` so you can call MCP tools, list resources, and
|
|
40
|
+
read resources from synchronous code (CLI tools, Django views, Flask
|
|
41
|
+
handlers, scripts). It also ships a CLI for ad-hoc server inspection
|
|
42
|
+
from the shell.
|
|
43
|
+
|
|
44
|
+
## Quick Start
|
|
45
|
+
|
|
46
|
+
Install from source:
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
pip install git+https://github.com/prasad-a-abhishek/mcpsync.git
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
Connect to a stdio MCP server and call a tool:
|
|
53
|
+
|
|
54
|
+
```python
|
|
55
|
+
from mcpsync import SyncMCPClient, StdioServerParameters
|
|
56
|
+
|
|
57
|
+
params = StdioServerParameters(
|
|
58
|
+
command="python", args=("-m", "my_mcp_server"),
|
|
59
|
+
env={"DEBUG": "1"},
|
|
60
|
+
cwd="/srv/myapp",
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
with SyncMCPClient(params) as client:
|
|
64
|
+
tools = client.list_tools()
|
|
65
|
+
for tool in tools:
|
|
66
|
+
print(f"- {tool.name}: {tool.description}")
|
|
67
|
+
|
|
68
|
+
result = client.call_tool("add", {"a": 2, "b": 3})
|
|
69
|
+
for block in result.content:
|
|
70
|
+
print(block.text)
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
Connect to an HTTP MCP server:
|
|
74
|
+
|
|
75
|
+
```python
|
|
76
|
+
from mcpsync import SyncMCPClient, HttpServerParameters
|
|
77
|
+
|
|
78
|
+
params = HttpServerParameters(
|
|
79
|
+
url="https://mcp.example.com/api",
|
|
80
|
+
headers={"Authorization": "Bearer ..."},
|
|
81
|
+
timeout=10.0,
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
with SyncMCPClient(params) as client:
|
|
85
|
+
resources = client.list_resources()
|
|
86
|
+
contents = client.read_resource(resources[0].uri)
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
Turn any async MCP helper into a sync function with the `@sync`
|
|
90
|
+
decorator:
|
|
91
|
+
|
|
92
|
+
```python
|
|
93
|
+
from mcpsync import sync
|
|
94
|
+
|
|
95
|
+
@sync
|
|
96
|
+
async def load_schema(server_url: str) -> dict:
|
|
97
|
+
async with some_async_helper(server_url) as helper:
|
|
98
|
+
return await helper.fetch_schema()
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
## ⚡ Performance & Benchmarks
|
|
102
|
+
|
|
103
|
+
We publish `benchmarks/BENCHMARK.md` per Invariant 14 — including
|
|
104
|
+
the methodology caveat. The honest summary: mcpsync uses
|
|
105
|
+
`asyncio.Runner` per call, which actually benchmarks ~15% faster
|
|
106
|
+
than the SDK's `asyncio.run` pattern in this setup (interpreter
|
|
107
|
+
boot dominates; relative comparison is meaningful). Reproduce
|
|
108
|
+
locally:
|
|
109
|
+
|
|
110
|
+
```bash
|
|
111
|
+
python benchmarks/run_benchmark.py
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
The full results table, methodology, and the trade-off
|
|
115
|
+
transparency statement are in
|
|
116
|
+
[benchmarks/BENCHMARK.md](benchmarks/BENCHMARK.md).
|
|
117
|
+
|
|
118
|
+
## Why `mcpsync`? (Problem & Trade-Off Statement)
|
|
119
|
+
|
|
120
|
+
The MCP Python SDK ships an **async-only** client. Synchronous callers
|
|
121
|
+
(Django views, CLI tools, scripts) hit the same wall: every
|
|
122
|
+
`asyncio.run()` from sync code re-creates an event loop, leaks
|
|
123
|
+
resources, and breaks under `asyncio.run()` recursion if the
|
|
124
|
+
surrounding runtime already runs a loop.
|
|
125
|
+
|
|
126
|
+
Two GitHub issues document the gap:
|
|
127
|
+
|
|
128
|
+
- [`modelcontextprotocol/python-sdk#1223`](https://github.com/modelcontextprotocol/python-sdk/issues/1223) — "Sync client API" (open, multiple reactions)
|
|
129
|
+
- `modelcontextprotocol/python-sdk` — the SDK explicitly documents
|
|
130
|
+
the `client.session.stdio` use pattern as **async only**.
|
|
131
|
+
|
|
132
|
+
**What mcpsync is:** a thin wrapper that gives you `SyncMCPClient` +
|
|
133
|
+
a `@sync` decorator, plus a CLI for ad-hoc inspection. It uses the
|
|
134
|
+
official `mcp` SDK under the hood and never re-implements the JSON-RPC
|
|
135
|
+
protocol or transport.
|
|
136
|
+
|
|
137
|
+
**What mcpsync is NOT:**
|
|
138
|
+
- Not a replacement for the `mcp` SDK — it depends on it.
|
|
139
|
+
- Not an MCP server implementation. It's a client.
|
|
140
|
+
- Not magic. Each `SyncMCPClient.list_tools()` call creates a
|
|
141
|
+
one-shot event loop on a worker thread. If you need 100k calls/sec,
|
|
142
|
+
use the SDK's async client directly.
|
|
143
|
+
|
|
144
|
+
**Trade-offs you accept by using mcpsync:**
|
|
145
|
+
- Each `SyncMCPClient` call creates a fresh `asyncio.Runner` on a
|
|
146
|
+
worker thread. The bench shows this is ~15% faster than the SDK
|
|
147
|
+
baseline, but for a true long-lived async loop you should use
|
|
148
|
+
the SDK's async client directly.
|
|
149
|
+
- The close path needs a wall-clock fuse to escape a known deadlock
|
|
150
|
+
in the `mcp` SDK's `stdio_client.__aexit__` shielded cancel scope.
|
|
151
|
+
Without the fuse the caller's process hangs. See
|
|
152
|
+
`src/mcpsync/client.py::_run_bounded` and the docstring on
|
|
153
|
+
`SyncMCPClient.close()`.
|
|
154
|
+
- HTTP transport requires the `mcp` SDK's optional `httpx2` dep,
|
|
155
|
+
which is re-exported by `mcp` for convenience.
|
|
156
|
+
|
|
157
|
+
## Key Features & Complete API / CLI Reference
|
|
158
|
+
|
|
159
|
+
### Library API
|
|
160
|
+
|
|
161
|
+
| Name | Returns | Notes |
|
|
162
|
+
|---|---|---|
|
|
163
|
+
| `StdioServerParameters(command, args, env, cwd)` | dataclass | spawn the server as a subprocess |
|
|
164
|
+
| `HttpServerParameters(url, headers, timeout)` | dataclass | connect to a streamable-HTTP MCP server |
|
|
165
|
+
| `SyncMCPClient(params)` | context manager | open the session; use as `with` block |
|
|
166
|
+
| `client.list_tools()` | `list[Tool]` | tool descriptors from `mcp.types.Tool` |
|
|
167
|
+
| `client.list_resources()` | `list[Resource]` | resource descriptors from `mcp.types.Resource` |
|
|
168
|
+
| `client.call_tool(name, arguments)` | `CallToolResult` | invoke a tool; `result.content` is a list of typed blocks |
|
|
169
|
+
| `client.read_resource(uri)` | `ReadResourceResult` | fetch a resource by URI |
|
|
170
|
+
| `@sync` decorator | wraps an async function into a sync one | uses a per-call event loop |
|
|
171
|
+
| `MCPError` | exception | re-exported from `mcp.shared.exceptions` |
|
|
172
|
+
|
|
173
|
+
All public types are fully type-hinted. A `py.typed` marker ships in
|
|
174
|
+
`src/mcpsync/` for PEP 561.
|
|
175
|
+
|
|
176
|
+
### CLI
|
|
177
|
+
|
|
178
|
+
```text
|
|
179
|
+
$ mcpsync --help
|
|
180
|
+
usage: mcpsync [-h] [--version] {stdio,http} ...
|
|
181
|
+
|
|
182
|
+
Synchronous client for MCP servers.
|
|
183
|
+
|
|
184
|
+
$ mcpsync stdio list-tools -- python -m my_server
|
|
185
|
+
[
|
|
186
|
+
{"name": "echo", "description": "Echo the input message back", "inputSchema": {...}},
|
|
187
|
+
...
|
|
188
|
+
]
|
|
189
|
+
|
|
190
|
+
$ mcpsync stdio call-tool add '{"a":2,"b":3}' -- python -m my_server
|
|
191
|
+
{"content":[{"type":"text","text":"5"}]}
|
|
192
|
+
|
|
193
|
+
$ mcpsync stdio list-resources -- python -m my_server
|
|
194
|
+
[{"uri": "file:///greeting.txt", "name": "greeting", "mimeType": "text/plain"}, ...]
|
|
195
|
+
|
|
196
|
+
$ mcpsync stdio call-resource 'file:///greeting.txt' -- python -m my_server
|
|
197
|
+
{"contents":[{"uri": "file:///greeting.txt", "text": "Hello!", "mimeType": "text/plain"}]}
|
|
198
|
+
|
|
199
|
+
$ mcpsync http list-tools --url https://mcp.example.com/api
|
|
200
|
+
$ mcpsync http call-tool add '{"a":2}' --url https://mcp.example.com/api
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
The `stdio` subcommand takes a `--` separator before the server
|
|
204
|
+
command + args. The `http` subcommand takes `--url`, `--header` (repeatable
|
|
205
|
+
`KEY=VAL`), and `--timeout`.
|
|
206
|
+
|
|
207
|
+
### Out of scope
|
|
208
|
+
|
|
209
|
+
- MCP server implementation
|
|
210
|
+
- Long-lived event-loop reuse across calls (each call creates a new
|
|
211
|
+
one; see trade-off above)
|
|
212
|
+
- Streaming / subscription primitives
|
|
213
|
+
- Server-side protocol: `mcpsync` is a client only
|
|
214
|
+
- Any protocol version below what the bundled `mcp` SDK supports
|
|
215
|
+
|
|
216
|
+
### Limitations
|
|
217
|
+
|
|
218
|
+
- **One shot per call.** Each `SyncMCPClient` use opens the server
|
|
219
|
+
process, runs the request, and closes. If you need a long-lived
|
|
220
|
+
connection, keep the `with` block open and make many calls inside.
|
|
221
|
+
- **HTTP transport** requires the `httpx2` runtime dep (re-exported
|
|
222
|
+
by `mcp` for convenience).
|
|
223
|
+
- **Close path** is bounded by a wall-clock fuse to avoid a known
|
|
224
|
+
deadlock in the `mcp` SDK's stdio teardown.
|
|
225
|
+
|
|
226
|
+
## Tests
|
|
227
|
+
|
|
228
|
+
```bash
|
|
229
|
+
pytest tests/ -q
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
129 tests across 1 file (split into 16 test classes by behavior).
|
|
233
|
+
Coverage spans every spec acceptance criterion plus angular sweep
|
|
234
|
+
(empty/None inputs, unicode, large payloads, malformed JSON, CLI
|
|
235
|
+
end-to-end, concurrent `@sync` calls, parameter adversarial
|
|
236
|
+
inputs, and regression guards for the close-path deadlock).
|
|
237
|
+
|
|
238
|
+
## License
|
|
239
|
+
|
|
240
|
+
MIT — see [LICENSE](LICENSE).
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
# mcpsync
|
|
2
|
+
|
|
3
|
+
[](https://pypi.org/project/mcpsync)
|
|
4
|
+
[](LICENSE)
|
|
5
|
+
[](https://www.python.org/)
|
|
6
|
+
[](tests/)
|
|
7
|
+
[](pyproject.toml)
|
|
8
|
+
|
|
9
|
+
> **Synchronous Python API for MCP servers — stdio and HTTP transports, plus a CLI for ad-hoc inspection.**
|
|
10
|
+
|
|
11
|
+
`mcpsync` wraps the official `mcp` Python SDK's async client behind a
|
|
12
|
+
blocking `SyncMCPClient` so you can call MCP tools, list resources, and
|
|
13
|
+
read resources from synchronous code (CLI tools, Django views, Flask
|
|
14
|
+
handlers, scripts). It also ships a CLI for ad-hoc server inspection
|
|
15
|
+
from the shell.
|
|
16
|
+
|
|
17
|
+
## Quick Start
|
|
18
|
+
|
|
19
|
+
Install from source:
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
pip install git+https://github.com/prasad-a-abhishek/mcpsync.git
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
Connect to a stdio MCP server and call a tool:
|
|
26
|
+
|
|
27
|
+
```python
|
|
28
|
+
from mcpsync import SyncMCPClient, StdioServerParameters
|
|
29
|
+
|
|
30
|
+
params = StdioServerParameters(
|
|
31
|
+
command="python", args=("-m", "my_mcp_server"),
|
|
32
|
+
env={"DEBUG": "1"},
|
|
33
|
+
cwd="/srv/myapp",
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
with SyncMCPClient(params) as client:
|
|
37
|
+
tools = client.list_tools()
|
|
38
|
+
for tool in tools:
|
|
39
|
+
print(f"- {tool.name}: {tool.description}")
|
|
40
|
+
|
|
41
|
+
result = client.call_tool("add", {"a": 2, "b": 3})
|
|
42
|
+
for block in result.content:
|
|
43
|
+
print(block.text)
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
Connect to an HTTP MCP server:
|
|
47
|
+
|
|
48
|
+
```python
|
|
49
|
+
from mcpsync import SyncMCPClient, HttpServerParameters
|
|
50
|
+
|
|
51
|
+
params = HttpServerParameters(
|
|
52
|
+
url="https://mcp.example.com/api",
|
|
53
|
+
headers={"Authorization": "Bearer ..."},
|
|
54
|
+
timeout=10.0,
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
with SyncMCPClient(params) as client:
|
|
58
|
+
resources = client.list_resources()
|
|
59
|
+
contents = client.read_resource(resources[0].uri)
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
Turn any async MCP helper into a sync function with the `@sync`
|
|
63
|
+
decorator:
|
|
64
|
+
|
|
65
|
+
```python
|
|
66
|
+
from mcpsync import sync
|
|
67
|
+
|
|
68
|
+
@sync
|
|
69
|
+
async def load_schema(server_url: str) -> dict:
|
|
70
|
+
async with some_async_helper(server_url) as helper:
|
|
71
|
+
return await helper.fetch_schema()
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
## ⚡ Performance & Benchmarks
|
|
75
|
+
|
|
76
|
+
We publish `benchmarks/BENCHMARK.md` per Invariant 14 — including
|
|
77
|
+
the methodology caveat. The honest summary: mcpsync uses
|
|
78
|
+
`asyncio.Runner` per call, which actually benchmarks ~15% faster
|
|
79
|
+
than the SDK's `asyncio.run` pattern in this setup (interpreter
|
|
80
|
+
boot dominates; relative comparison is meaningful). Reproduce
|
|
81
|
+
locally:
|
|
82
|
+
|
|
83
|
+
```bash
|
|
84
|
+
python benchmarks/run_benchmark.py
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
The full results table, methodology, and the trade-off
|
|
88
|
+
transparency statement are in
|
|
89
|
+
[benchmarks/BENCHMARK.md](benchmarks/BENCHMARK.md).
|
|
90
|
+
|
|
91
|
+
## Why `mcpsync`? (Problem & Trade-Off Statement)
|
|
92
|
+
|
|
93
|
+
The MCP Python SDK ships an **async-only** client. Synchronous callers
|
|
94
|
+
(Django views, CLI tools, scripts) hit the same wall: every
|
|
95
|
+
`asyncio.run()` from sync code re-creates an event loop, leaks
|
|
96
|
+
resources, and breaks under `asyncio.run()` recursion if the
|
|
97
|
+
surrounding runtime already runs a loop.
|
|
98
|
+
|
|
99
|
+
Two GitHub issues document the gap:
|
|
100
|
+
|
|
101
|
+
- [`modelcontextprotocol/python-sdk#1223`](https://github.com/modelcontextprotocol/python-sdk/issues/1223) — "Sync client API" (open, multiple reactions)
|
|
102
|
+
- `modelcontextprotocol/python-sdk` — the SDK explicitly documents
|
|
103
|
+
the `client.session.stdio` use pattern as **async only**.
|
|
104
|
+
|
|
105
|
+
**What mcpsync is:** a thin wrapper that gives you `SyncMCPClient` +
|
|
106
|
+
a `@sync` decorator, plus a CLI for ad-hoc inspection. It uses the
|
|
107
|
+
official `mcp` SDK under the hood and never re-implements the JSON-RPC
|
|
108
|
+
protocol or transport.
|
|
109
|
+
|
|
110
|
+
**What mcpsync is NOT:**
|
|
111
|
+
- Not a replacement for the `mcp` SDK — it depends on it.
|
|
112
|
+
- Not an MCP server implementation. It's a client.
|
|
113
|
+
- Not magic. Each `SyncMCPClient.list_tools()` call creates a
|
|
114
|
+
one-shot event loop on a worker thread. If you need 100k calls/sec,
|
|
115
|
+
use the SDK's async client directly.
|
|
116
|
+
|
|
117
|
+
**Trade-offs you accept by using mcpsync:**
|
|
118
|
+
- Each `SyncMCPClient` call creates a fresh `asyncio.Runner` on a
|
|
119
|
+
worker thread. The bench shows this is ~15% faster than the SDK
|
|
120
|
+
baseline, but for a true long-lived async loop you should use
|
|
121
|
+
the SDK's async client directly.
|
|
122
|
+
- The close path needs a wall-clock fuse to escape a known deadlock
|
|
123
|
+
in the `mcp` SDK's `stdio_client.__aexit__` shielded cancel scope.
|
|
124
|
+
Without the fuse the caller's process hangs. See
|
|
125
|
+
`src/mcpsync/client.py::_run_bounded` and the docstring on
|
|
126
|
+
`SyncMCPClient.close()`.
|
|
127
|
+
- HTTP transport requires the `mcp` SDK's optional `httpx2` dep,
|
|
128
|
+
which is re-exported by `mcp` for convenience.
|
|
129
|
+
|
|
130
|
+
## Key Features & Complete API / CLI Reference
|
|
131
|
+
|
|
132
|
+
### Library API
|
|
133
|
+
|
|
134
|
+
| Name | Returns | Notes |
|
|
135
|
+
|---|---|---|
|
|
136
|
+
| `StdioServerParameters(command, args, env, cwd)` | dataclass | spawn the server as a subprocess |
|
|
137
|
+
| `HttpServerParameters(url, headers, timeout)` | dataclass | connect to a streamable-HTTP MCP server |
|
|
138
|
+
| `SyncMCPClient(params)` | context manager | open the session; use as `with` block |
|
|
139
|
+
| `client.list_tools()` | `list[Tool]` | tool descriptors from `mcp.types.Tool` |
|
|
140
|
+
| `client.list_resources()` | `list[Resource]` | resource descriptors from `mcp.types.Resource` |
|
|
141
|
+
| `client.call_tool(name, arguments)` | `CallToolResult` | invoke a tool; `result.content` is a list of typed blocks |
|
|
142
|
+
| `client.read_resource(uri)` | `ReadResourceResult` | fetch a resource by URI |
|
|
143
|
+
| `@sync` decorator | wraps an async function into a sync one | uses a per-call event loop |
|
|
144
|
+
| `MCPError` | exception | re-exported from `mcp.shared.exceptions` |
|
|
145
|
+
|
|
146
|
+
All public types are fully type-hinted. A `py.typed` marker ships in
|
|
147
|
+
`src/mcpsync/` for PEP 561.
|
|
148
|
+
|
|
149
|
+
### CLI
|
|
150
|
+
|
|
151
|
+
```text
|
|
152
|
+
$ mcpsync --help
|
|
153
|
+
usage: mcpsync [-h] [--version] {stdio,http} ...
|
|
154
|
+
|
|
155
|
+
Synchronous client for MCP servers.
|
|
156
|
+
|
|
157
|
+
$ mcpsync stdio list-tools -- python -m my_server
|
|
158
|
+
[
|
|
159
|
+
{"name": "echo", "description": "Echo the input message back", "inputSchema": {...}},
|
|
160
|
+
...
|
|
161
|
+
]
|
|
162
|
+
|
|
163
|
+
$ mcpsync stdio call-tool add '{"a":2,"b":3}' -- python -m my_server
|
|
164
|
+
{"content":[{"type":"text","text":"5"}]}
|
|
165
|
+
|
|
166
|
+
$ mcpsync stdio list-resources -- python -m my_server
|
|
167
|
+
[{"uri": "file:///greeting.txt", "name": "greeting", "mimeType": "text/plain"}, ...]
|
|
168
|
+
|
|
169
|
+
$ mcpsync stdio call-resource 'file:///greeting.txt' -- python -m my_server
|
|
170
|
+
{"contents":[{"uri": "file:///greeting.txt", "text": "Hello!", "mimeType": "text/plain"}]}
|
|
171
|
+
|
|
172
|
+
$ mcpsync http list-tools --url https://mcp.example.com/api
|
|
173
|
+
$ mcpsync http call-tool add '{"a":2}' --url https://mcp.example.com/api
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
The `stdio` subcommand takes a `--` separator before the server
|
|
177
|
+
command + args. The `http` subcommand takes `--url`, `--header` (repeatable
|
|
178
|
+
`KEY=VAL`), and `--timeout`.
|
|
179
|
+
|
|
180
|
+
### Out of scope
|
|
181
|
+
|
|
182
|
+
- MCP server implementation
|
|
183
|
+
- Long-lived event-loop reuse across calls (each call creates a new
|
|
184
|
+
one; see trade-off above)
|
|
185
|
+
- Streaming / subscription primitives
|
|
186
|
+
- Server-side protocol: `mcpsync` is a client only
|
|
187
|
+
- Any protocol version below what the bundled `mcp` SDK supports
|
|
188
|
+
|
|
189
|
+
### Limitations
|
|
190
|
+
|
|
191
|
+
- **One shot per call.** Each `SyncMCPClient` use opens the server
|
|
192
|
+
process, runs the request, and closes. If you need a long-lived
|
|
193
|
+
connection, keep the `with` block open and make many calls inside.
|
|
194
|
+
- **HTTP transport** requires the `httpx2` runtime dep (re-exported
|
|
195
|
+
by `mcp` for convenience).
|
|
196
|
+
- **Close path** is bounded by a wall-clock fuse to avoid a known
|
|
197
|
+
deadlock in the `mcp` SDK's stdio teardown.
|
|
198
|
+
|
|
199
|
+
## Tests
|
|
200
|
+
|
|
201
|
+
```bash
|
|
202
|
+
pytest tests/ -q
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
129 tests across 1 file (split into 16 test classes by behavior).
|
|
206
|
+
Coverage spans every spec acceptance criterion plus angular sweep
|
|
207
|
+
(empty/None inputs, unicode, large payloads, malformed JSON, CLI
|
|
208
|
+
end-to-end, concurrent `@sync` calls, parameter adversarial
|
|
209
|
+
inputs, and regression guards for the close-path deadlock).
|
|
210
|
+
|
|
211
|
+
## License
|
|
212
|
+
|
|
213
|
+
MIT — see [LICENSE](LICENSE).
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "mcpsync-cli"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "Synchronous MCP Python client — call MCP servers from sync Python without async/await"
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
license = {text = "MIT"}
|
|
7
|
+
requires-python = ">=3.11"
|
|
8
|
+
authors = [
|
|
9
|
+
{name = "Repo Factory", email = "noreply@example.com"},
|
|
10
|
+
]
|
|
11
|
+
keywords = [
|
|
12
|
+
"mcp",
|
|
13
|
+
"model-context-protocol",
|
|
14
|
+
"synchronous",
|
|
15
|
+
"sync",
|
|
16
|
+
"client",
|
|
17
|
+
"stdio",
|
|
18
|
+
"http",
|
|
19
|
+
"streamable-http",
|
|
20
|
+
]
|
|
21
|
+
classifiers = [
|
|
22
|
+
"Development Status :: 4 - Beta",
|
|
23
|
+
"Intended Audience :: Developers",
|
|
24
|
+
"License :: OSI Approved :: MIT License",
|
|
25
|
+
"Operating System :: OS Independent",
|
|
26
|
+
"Programming Language :: Python :: 3",
|
|
27
|
+
"Programming Language :: Python :: 3.11",
|
|
28
|
+
"Programming Language :: Python :: 3.12",
|
|
29
|
+
"Topic :: Software Development :: Libraries :: Python Modules",
|
|
30
|
+
"Typing :: Typed",
|
|
31
|
+
]
|
|
32
|
+
# mcpsync wraps the official `mcp` SDK. It introduces NO deps beyond `mcp` + stdlib.
|
|
33
|
+
# See spec.md acceptance criterion #14: no third-party imports outside stdlib.
|
|
34
|
+
dependencies = [
|
|
35
|
+
"mcp>=1.0",
|
|
36
|
+
]
|
|
37
|
+
|
|
38
|
+
[project.optional-dependencies]
|
|
39
|
+
dev = [
|
|
40
|
+
"pytest>=7.0",
|
|
41
|
+
]
|
|
42
|
+
|
|
43
|
+
[project.scripts]
|
|
44
|
+
mcpsync = "mcpsync.cli:main"
|
|
45
|
+
|
|
46
|
+
[project.urls]
|
|
47
|
+
Homepage = "https://github.com/prasad-a-abhishek/mcpsync"
|
|
48
|
+
Repository = "https://github.com/prasad-a-abhishek/mcpsync"
|
|
49
|
+
Issues = "https://github.com/prasad-a-abhishek/mcpsync/issues"
|
|
50
|
+
|
|
51
|
+
[build-system]
|
|
52
|
+
requires = ["setuptools>=61.0"]
|
|
53
|
+
build-backend = "setuptools.build_meta"
|
|
54
|
+
|
|
55
|
+
[tool.setuptools.packages.find]
|
|
56
|
+
where = ["src"]
|
|
57
|
+
|
|
58
|
+
[tool.setuptools.package-data]
|
|
59
|
+
mcpsync = ["py.typed", "*.pyi"]
|
|
60
|
+
|
|
61
|
+
[tool.pytest.ini_options]
|
|
62
|
+
testpaths = ["tests"]
|
|
63
|
+
addopts = "-q"
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
"""``mcpsync`` — Synchronous MCP Python client.
|
|
2
|
+
|
|
3
|
+
Call MCP servers from sync Python without ``async``/``await``. Wraps the
|
|
4
|
+
official `mcp` SDK's async ``ClientSession`` inside a dedicated
|
|
5
|
+
``asyncio.Runner`` per session, and exposes ``call_tool``, ``list_tools``,
|
|
6
|
+
``call_resource``, and ``list_resources`` as plain blocking methods.
|
|
7
|
+
|
|
8
|
+
Example::
|
|
9
|
+
|
|
10
|
+
from mcpsync import SyncMCPClient, StdioServerParameters
|
|
11
|
+
|
|
12
|
+
params = StdioServerParameters(command="python", args=["-m", "my_server"])
|
|
13
|
+
with SyncMCPClient(params) as client:
|
|
14
|
+
tools = client.list_tools()
|
|
15
|
+
result = client.call_tool("git_log", {"n": 5})
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
from mcpsync.client import SyncMCPClient
|
|
19
|
+
from mcpsync.errors import MCPError
|
|
20
|
+
from mcpsync.params import HttpServerParameters, StdioServerParameters
|
|
21
|
+
from mcpsync.sync import sync
|
|
22
|
+
|
|
23
|
+
__all__ = [
|
|
24
|
+
"HttpServerParameters",
|
|
25
|
+
"MCPError",
|
|
26
|
+
"StdioServerParameters",
|
|
27
|
+
"SyncMCPClient",
|
|
28
|
+
"sync",
|
|
29
|
+
]
|
|
30
|
+
|
|
31
|
+
__version__ = "0.1.0"
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
"""Type stubs for the ``mcpsync`` public API."""
|
|
2
|
+
|
|
3
|
+
from collections.abc import Callable, Mapping
|
|
4
|
+
from types import TracebackType
|
|
5
|
+
from typing import Any, ParamSpec, TypeVar
|
|
6
|
+
|
|
7
|
+
from typing_extensions import Self
|
|
8
|
+
|
|
9
|
+
P = ParamSpec("P")
|
|
10
|
+
R = TypeVar("R")
|
|
11
|
+
|
|
12
|
+
class StdioServerParameters:
|
|
13
|
+
command: str
|
|
14
|
+
args: tuple[str, ...]
|
|
15
|
+
env: Mapping[str, str] | None
|
|
16
|
+
cwd: str | None
|
|
17
|
+
def __init__(
|
|
18
|
+
self,
|
|
19
|
+
command: str,
|
|
20
|
+
args: tuple[str, ...] | list[str] = (),
|
|
21
|
+
env: Mapping[str, str] | None = None,
|
|
22
|
+
cwd: str | None = None,
|
|
23
|
+
) -> None: ...
|
|
24
|
+
def to_argv(self) -> list[str]: ...
|
|
25
|
+
|
|
26
|
+
class HttpServerParameters:
|
|
27
|
+
url: str
|
|
28
|
+
headers: Mapping[str, str]
|
|
29
|
+
timeout: float
|
|
30
|
+
def __init__(
|
|
31
|
+
self,
|
|
32
|
+
url: str,
|
|
33
|
+
headers: Mapping[str, str] | None = None,
|
|
34
|
+
timeout: float = 30.0,
|
|
35
|
+
) -> None: ...
|
|
36
|
+
|
|
37
|
+
class MCPError(Exception):
|
|
38
|
+
code: int
|
|
39
|
+
message: str
|
|
40
|
+
data: Any
|
|
41
|
+
def __init__(self, code: int, message: str, data: Any = None) -> None: ...
|
|
42
|
+
|
|
43
|
+
class SyncMCPClient:
|
|
44
|
+
def __init__(
|
|
45
|
+
self,
|
|
46
|
+
params: StdioServerParameters | HttpServerParameters,
|
|
47
|
+
*,
|
|
48
|
+
timeout: float | None = None,
|
|
49
|
+
) -> None: ...
|
|
50
|
+
def __enter__(self) -> Self: ...
|
|
51
|
+
def __exit__(
|
|
52
|
+
self,
|
|
53
|
+
exc_type: type[BaseException] | None,
|
|
54
|
+
exc: BaseException | None,
|
|
55
|
+
tb: TracebackType | None,
|
|
56
|
+
) -> None: ...
|
|
57
|
+
def list_tools(self) -> list[Any]: ...
|
|
58
|
+
def call_tool(self, name: str, arguments: dict[str, Any] | None = None) -> Any: ...
|
|
59
|
+
def list_resources(self) -> list[Any]: ...
|
|
60
|
+
def call_resource(self, uri: str) -> Any: ...
|
|
61
|
+
def read_resource(self, uri: str) -> Any: ...
|
|
62
|
+
def close(self) -> None: ...
|
|
63
|
+
|
|
64
|
+
def sync(fn: Callable[P, Any]) -> Callable[P, Any]: ...
|