pydantic-rpc 0.6.1__tar.gz → 0.7.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.
- pydantic_rpc-0.7.0/.github/workflows/release.yml +35 -0
- pydantic_rpc-0.7.0/.github/workflows/test.yml +47 -0
- pydantic_rpc-0.7.0/.gitignore +24 -0
- pydantic_rpc-0.6.1/README.md → pydantic_rpc-0.7.0/PKG-INFO +162 -8
- pydantic_rpc-0.6.1/PKG-INFO → pydantic_rpc-0.7.0/README.md +143 -23
- pydantic_rpc-0.7.0/docs/mcp.md +95 -0
- {pydantic_rpc-0.6.1 → pydantic_rpc-0.7.0}/examples/asyncio_greeting.py +22 -22
- {pydantic_rpc-0.6.1 → pydantic_rpc-0.7.0}/examples/barservice.proto +17 -17
- {pydantic_rpc-0.6.1 → pydantic_rpc-0.7.0}/examples/foobar.py +76 -76
- {pydantic_rpc-0.6.1 → pydantic_rpc-0.7.0}/examples/foobar_client.py +20 -20
- {pydantic_rpc-0.6.1 → pydantic_rpc-0.7.0}/examples/fooservice.proto +21 -21
- {pydantic_rpc-0.6.1 → pydantic_rpc-0.7.0}/examples/google/protobuf/duration.proto +115 -115
- {pydantic_rpc-0.6.1 → pydantic_rpc-0.7.0}/examples/google/protobuf/timestamp.proto +144 -144
- {pydantic_rpc-0.6.1 → pydantic_rpc-0.7.0}/examples/greeter.proto +35 -35
- pydantic_rpc-0.7.0/examples/greeter_connecpy.py +124 -0
- {pydantic_rpc-0.6.1 → pydantic_rpc-0.7.0}/examples/greeter_connecpy_client.py +10 -9
- {pydantic_rpc-0.6.1 → pydantic_rpc-0.7.0}/examples/greeter_pb2_grpc.py +4 -17
- {pydantic_rpc-0.6.1 → pydantic_rpc-0.7.0}/examples/greeting.py +45 -45
- {pydantic_rpc-0.6.1 → pydantic_rpc-0.7.0}/examples/greeting_asgi.py +55 -55
- {pydantic_rpc-0.6.1 → pydantic_rpc-0.7.0}/examples/greeting_wsgi.py +63 -63
- pydantic_rpc-0.7.0/examples/mcp_debug_example.py +74 -0
- pydantic_rpc-0.7.0/examples/mcp_example.py +129 -0
- pydantic_rpc-0.7.0/examples/mcp_http_example.py +125 -0
- pydantic_rpc-0.7.0/examples/mcp_simple_calculator.py +45 -0
- {pydantic_rpc-0.6.1 → pydantic_rpc-0.7.0}/examples/olympicsagent.proto +40 -40
- {pydantic_rpc-0.6.1 → pydantic_rpc-0.7.0}/examples/olympicslocationagent.proto +24 -24
- {pydantic_rpc-0.6.1 → pydantic_rpc-0.7.0}/pyproject.toml +8 -2
- {pydantic_rpc-0.6.1 → pydantic_rpc-0.7.0}/src/pydantic_rpc/__init__.py +10 -0
- pydantic_rpc-0.7.0/src/pydantic_rpc/core.py +2445 -0
- pydantic_rpc-0.7.0/src/pydantic_rpc/mcp/__init__.py +5 -0
- pydantic_rpc-0.7.0/src/pydantic_rpc/mcp/converter.py +115 -0
- pydantic_rpc-0.7.0/src/pydantic_rpc/mcp/exporter.py +283 -0
- {pydantic_rpc-0.6.1 → pydantic_rpc-0.7.0}/tests/asyncechoservice.proto +33 -33
- {pydantic_rpc-0.6.1 → pydantic_rpc-0.7.0}/tests/echoservice.proto +33 -33
- pydantic_rpc-0.7.0/tests/google_protobuf/greeterwithduration.proto +14 -0
- pydantic_rpc-0.7.0/tests/google_protobuf/greeterwithtimestamp.proto +14 -0
- pydantic_rpc-0.7.0/tests/google_protobuf/test_google_protobuf.py +41 -0
- pydantic_rpc-0.7.0/tests/greeterwithduration.proto +14 -0
- pydantic_rpc-0.7.0/tests/greeterwithtimestamp.proto +14 -0
- pydantic_rpc-0.7.0/tests/test_apps.py +378 -0
- pydantic_rpc-0.7.0/tests/test_conversion.py +1126 -0
- pydantic_rpc-0.7.0/tests/test_mcp.py +181 -0
- pydantic_rpc-0.7.0/tests/test_utils.py +511 -0
- {pydantic_rpc-0.6.1 → pydantic_rpc-0.7.0}/uv.lock +758 -846
- pydantic_rpc-0.6.1/.gitignore +0 -13
- pydantic_rpc-0.6.1/examples/greeter_connecpy.py +0 -111
- pydantic_rpc-0.6.1/src/pydantic_rpc/core.py +0 -1455
- pydantic_rpc-0.6.1/tests/asyncechoservice_connecpy.py +0 -109
- pydantic_rpc-0.6.1/tests/asyncechoservice_pb2.py +0 -37
- pydantic_rpc-0.6.1/tests/asyncechoservice_pb2.pyi +0 -17
- pydantic_rpc-0.6.1/tests/asyncechoservice_pb2_grpc.py +0 -121
- pydantic_rpc-0.6.1/tests/echoservice_connecpy.py +0 -109
- pydantic_rpc-0.6.1/tests/echoservice_pb2.py +0 -37
- pydantic_rpc-0.6.1/tests/echoservice_pb2.pyi +0 -17
- pydantic_rpc-0.6.1/tests/echoservice_pb2_grpc.py +0 -119
- pydantic_rpc-0.6.1/tests/test_apps.py +0 -190
- {pydantic_rpc-0.6.1 → pydantic_rpc-0.7.0}/.python-version +0 -0
- {pydantic_rpc-0.6.1 → pydantic_rpc-0.7.0}/LICENSE +0 -0
- {pydantic_rpc-0.6.1 → pydantic_rpc-0.7.0}/examples/README.md +0 -0
- {pydantic_rpc-0.6.1 → pydantic_rpc-0.7.0}/examples/agent_aio_grpc.py +0 -0
- {pydantic_rpc-0.6.1 → pydantic_rpc-0.7.0}/examples/agent_connecpy.py +0 -0
- {pydantic_rpc-0.6.1 → pydantic_rpc-0.7.0}/examples/barservice_pb2.py +0 -0
- {pydantic_rpc-0.6.1 → pydantic_rpc-0.7.0}/examples/barservice_pb2.pyi +0 -0
- {pydantic_rpc-0.6.1 → pydantic_rpc-0.7.0}/examples/barservice_pb2_grpc.py +0 -0
- {pydantic_rpc-0.6.1 → pydantic_rpc-0.7.0}/examples/fooservice_pb2.py +0 -0
- {pydantic_rpc-0.6.1 → pydantic_rpc-0.7.0}/examples/fooservice_pb2.pyi +0 -0
- {pydantic_rpc-0.6.1 → pydantic_rpc-0.7.0}/examples/fooservice_pb2_grpc.py +0 -0
- {pydantic_rpc-0.6.1 → pydantic_rpc-0.7.0}/examples/greeter_client.py +0 -0
- {pydantic_rpc-0.6.1 → pydantic_rpc-0.7.0}/examples/greeter_pb2.py +0 -0
- {pydantic_rpc-0.6.1 → pydantic_rpc-0.7.0}/examples/greeter_pb2.pyi +0 -0
- {pydantic_rpc-0.6.1 → pydantic_rpc-0.7.0}/examples/greeter_sonora_client.py +0 -0
- {pydantic_rpc-0.6.1 → pydantic_rpc-0.7.0}/examples/greeting_connecpy.py +0 -0
- {pydantic_rpc-0.6.1 → pydantic_rpc-0.7.0}/examples/greeting_using_exsiting_pb2_modules.py +0 -0
- {pydantic_rpc-0.6.1 → pydantic_rpc-0.7.0}/examples/olympicsagent_pb2.py +0 -0
- {pydantic_rpc-0.6.1 → pydantic_rpc-0.7.0}/examples/olympicsagent_pb2.pyi +0 -0
- {pydantic_rpc-0.6.1 → pydantic_rpc-0.7.0}/examples/olympicsagent_pb2_grpc.py +0 -0
- {pydantic_rpc-0.6.1 → pydantic_rpc-0.7.0}/examples/olympicslocationagent_connecpy.py +0 -0
- {pydantic_rpc-0.6.1 → pydantic_rpc-0.7.0}/examples/olympicslocationagent_pb2.py +0 -0
- {pydantic_rpc-0.6.1 → pydantic_rpc-0.7.0}/examples/olympicslocationagent_pb2.pyi +0 -0
- {pydantic_rpc-0.6.1 → pydantic_rpc-0.7.0}/examples/olympicslocationagent_pb2_grpc.py +0 -0
- {pydantic_rpc-0.6.1 → pydantic_rpc-0.7.0}/src/pydantic_rpc/py.typed +0 -0
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
name: Release
|
|
2
|
+
on:
|
|
3
|
+
push:
|
|
4
|
+
tags:
|
|
5
|
+
- "v*"
|
|
6
|
+
workflow_dispatch:
|
|
7
|
+
|
|
8
|
+
permissions:
|
|
9
|
+
id-token: write
|
|
10
|
+
attestations: write
|
|
11
|
+
contents: write
|
|
12
|
+
|
|
13
|
+
jobs:
|
|
14
|
+
release:
|
|
15
|
+
runs-on: ubuntu-24.04
|
|
16
|
+
steps:
|
|
17
|
+
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
|
18
|
+
|
|
19
|
+
- uses: astral-sh/setup-uv@d4b2f3b6ecc6e67c4457f6d3e41ec42d3d0fcb86 # v5.4.2
|
|
20
|
+
|
|
21
|
+
- uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0
|
|
22
|
+
with:
|
|
23
|
+
python-version-file: "pyproject.toml"
|
|
24
|
+
|
|
25
|
+
- run: uv sync --frozen
|
|
26
|
+
|
|
27
|
+
- run: uv build
|
|
28
|
+
|
|
29
|
+
- uses: pypa/gh-action-pypi-publish@76f52bc884231f62b9a034ebfe128415bbaabdfc # v1.12.4
|
|
30
|
+
with:
|
|
31
|
+
password: ${{ secrets.PYPI_TOKEN }}
|
|
32
|
+
|
|
33
|
+
- uses: actions/attest-build-provenance@e8998f949152b193b063cb0ec769d69d929409be # v2.4.0
|
|
34
|
+
with:
|
|
35
|
+
subject-path: dist/*.tar.gz
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
name: Test
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
pull_request:
|
|
5
|
+
branches: [ main ]
|
|
6
|
+
push:
|
|
7
|
+
branches: [ main ]
|
|
8
|
+
|
|
9
|
+
jobs:
|
|
10
|
+
test:
|
|
11
|
+
runs-on: ubuntu-latest
|
|
12
|
+
strategy:
|
|
13
|
+
matrix:
|
|
14
|
+
python-version: ["3.11", "3.12", "3.13"]
|
|
15
|
+
fail-fast: false
|
|
16
|
+
|
|
17
|
+
steps:
|
|
18
|
+
- name: Checkout code
|
|
19
|
+
uses: actions/checkout@v4
|
|
20
|
+
|
|
21
|
+
- name: Setup Go
|
|
22
|
+
uses: actions/setup-go@v5
|
|
23
|
+
with:
|
|
24
|
+
go-version: '1.24'
|
|
25
|
+
cache: true
|
|
26
|
+
|
|
27
|
+
- name: Install protoc-gen-connecpy plugin
|
|
28
|
+
run: go install github.com/i2y/connecpy/v2/protoc-gen-connecpy@latest
|
|
29
|
+
|
|
30
|
+
- name: Setup uv with cache
|
|
31
|
+
id: setup-uv
|
|
32
|
+
uses: astral-sh/setup-uv@v6
|
|
33
|
+
with:
|
|
34
|
+
enable-cache: true
|
|
35
|
+
|
|
36
|
+
- name: Install Python ${{ matrix.python-version }}
|
|
37
|
+
run: uv python install ${{ matrix.python-version }}
|
|
38
|
+
|
|
39
|
+
- name: Install dependencies
|
|
40
|
+
run: uv sync
|
|
41
|
+
|
|
42
|
+
- name: Run tests
|
|
43
|
+
run: |
|
|
44
|
+
export PATH="$PATH:$(go env GOPATH)/bin"
|
|
45
|
+
uv run pytest
|
|
46
|
+
env:
|
|
47
|
+
PYTHONPATH: ${{ github.workspace }}/src
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# python generated files
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.py[oc]
|
|
4
|
+
build/
|
|
5
|
+
dist/
|
|
6
|
+
wheels/
|
|
7
|
+
*.egg-info
|
|
8
|
+
|
|
9
|
+
# venv
|
|
10
|
+
.venv
|
|
11
|
+
|
|
12
|
+
# vscode
|
|
13
|
+
.vscode
|
|
14
|
+
|
|
15
|
+
# development
|
|
16
|
+
.devcontainer/
|
|
17
|
+
.env
|
|
18
|
+
rpc_files/
|
|
19
|
+
|
|
20
|
+
# tests
|
|
21
|
+
tests/**/*.py
|
|
22
|
+
tests/**/*.pyi
|
|
23
|
+
!tests/**/test_*.py
|
|
24
|
+
.coverage
|
|
@@ -1,3 +1,22 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: pydantic-rpc
|
|
3
|
+
Version: 0.7.0
|
|
4
|
+
Summary: A Python library for building gRPC/ConnectRPC services with Pydantic models.
|
|
5
|
+
Author: Yasushi Itoh
|
|
6
|
+
License-File: LICENSE
|
|
7
|
+
Requires-Python: >=3.11
|
|
8
|
+
Requires-Dist: annotated-types>=0.5.0
|
|
9
|
+
Requires-Dist: connecpy==2.0.0
|
|
10
|
+
Requires-Dist: grpcio-health-checking>=1.56.2
|
|
11
|
+
Requires-Dist: grpcio-reflection>=1.56.2
|
|
12
|
+
Requires-Dist: grpcio-tools>=1.56.2
|
|
13
|
+
Requires-Dist: grpcio>=1.56.2
|
|
14
|
+
Requires-Dist: mcp>=1.9.4
|
|
15
|
+
Requires-Dist: pydantic>=2.1.1
|
|
16
|
+
Requires-Dist: sonora>=0.2.3
|
|
17
|
+
Requires-Dist: starlette>=0.27.0
|
|
18
|
+
Description-Content-Type: text/markdown
|
|
19
|
+
|
|
1
20
|
# 🚀 PydanticRPC
|
|
2
21
|
|
|
3
22
|
**PydanticRPC** is a Python library that enables you to rapidly expose [Pydantic](https://docs.pydantic.dev/) models via [gRPC](https://grpc.io/)/[Connect RPC](https://connectrpc.com/docs/protocol/) services without writing any protobuf files. Instead, it automatically generates protobuf files on the fly from the method signatures of your Python objects and the type signatures of your Pydantic models.
|
|
@@ -110,6 +129,7 @@ app.mount(OlympicsLocationAgent())
|
|
|
110
129
|
- **For Connect-RPC:**
|
|
111
130
|
- 🌐 **Connecpy Support:** Partially supports Connect-RPC via `Connecpy`.
|
|
112
131
|
- 🛠️ **Pre-generated Protobuf Files and Code:** Pre-generate proto files and corresponding code via the CLI. By setting the environment variable (PYDANTIC_RPC_SKIP_GENERATION), you can skip runtime generation.
|
|
132
|
+
- 🤖 **MCP (Model Context Protocol) Support:** Expose your services as tools for AI assistants using the official MCP SDK, supporting both stdio and HTTP/SSE transports.
|
|
113
133
|
|
|
114
134
|
## 📦 Installation
|
|
115
135
|
|
|
@@ -163,12 +183,18 @@ class Greeter:
|
|
|
163
183
|
return HelloReply(message=f"Hello, {request.name}!")
|
|
164
184
|
|
|
165
185
|
|
|
186
|
+
async def main():
|
|
187
|
+
# You can specify a custom port (default is 50051)
|
|
188
|
+
server = AsyncIOServer(port=50052)
|
|
189
|
+
await server.run(Greeter())
|
|
190
|
+
|
|
191
|
+
|
|
166
192
|
if __name__ == "__main__":
|
|
167
|
-
|
|
168
|
-
loop = asyncio.get_event_loop()
|
|
169
|
-
loop.run_until_complete(server.run(Greeter()))
|
|
193
|
+
asyncio.run(main())
|
|
170
194
|
```
|
|
171
195
|
|
|
196
|
+
The AsyncIOServer automatically handles graceful shutdown on SIGTERM and SIGINT signals.
|
|
197
|
+
|
|
172
198
|
### 🌐 ASGI Application Example
|
|
173
199
|
|
|
174
200
|
```python
|
|
@@ -248,7 +274,7 @@ This will launch a Connecpy-based ASGI application that uses the same Pydantic m
|
|
|
248
274
|
> - Please follow the instruction described in https://go.dev/doc/install.
|
|
249
275
|
> 2. Install `protoc-gen-connecpy`:
|
|
250
276
|
> ```bash
|
|
251
|
-
> go install github.com/connecpy/protoc-gen-connecpy@latest
|
|
277
|
+
> go install github.com/i2y/connecpy/v2/protoc-gen-connecpy@latest
|
|
252
278
|
> ```
|
|
253
279
|
|
|
254
280
|
## ♻️ Skipping Protobuf Generation
|
|
@@ -260,6 +286,21 @@ export PYDANTIC_RPC_SKIP_GENERATION=true
|
|
|
260
286
|
|
|
261
287
|
When this variable is set to "true", PydanticRPC will load existing pre-generated modules rather than generating them on the fly.
|
|
262
288
|
|
|
289
|
+
## 🪧 Setting Protobuf and Connecpy/gRPC generation directory
|
|
290
|
+
By default your files will be generated in the current working directory where you ran the code from, but you can set a custom specific directory by setting the environment variable below:
|
|
291
|
+
|
|
292
|
+
```bash
|
|
293
|
+
export PYDANTIC_RPC_PROTO_PATH=/your/path
|
|
294
|
+
```
|
|
295
|
+
|
|
296
|
+
## ⚠️ Reserved Fields
|
|
297
|
+
|
|
298
|
+
You can also set an environment variable to reserve a set number of fields for proto generation, for backward and forward compatibility.
|
|
299
|
+
|
|
300
|
+
```bash
|
|
301
|
+
export PYDANTIC_RPC_RESERVED_FIELDS=1
|
|
302
|
+
```
|
|
303
|
+
|
|
263
304
|
## 💎 Advanced Features
|
|
264
305
|
|
|
265
306
|
### 🌊 Response Streaming
|
|
@@ -718,6 +759,74 @@ if __name__ == "__main__":
|
|
|
718
759
|
|
|
719
760
|
TODO
|
|
720
761
|
|
|
762
|
+
### 🤖 MCP (Model Context Protocol) Support
|
|
763
|
+
|
|
764
|
+
PydanticRPC can expose your services as MCP tools for AI assistants using FastMCP. This enables seamless integration with any MCP-compatible client.
|
|
765
|
+
|
|
766
|
+
#### Stdio Mode Example
|
|
767
|
+
|
|
768
|
+
```python
|
|
769
|
+
from pydantic_rpc import Message
|
|
770
|
+
from pydantic_rpc.mcp import MCPExporter
|
|
771
|
+
|
|
772
|
+
class CalculateRequest(Message):
|
|
773
|
+
expression: str
|
|
774
|
+
|
|
775
|
+
class CalculateResponse(Message):
|
|
776
|
+
result: float
|
|
777
|
+
|
|
778
|
+
class MathService:
|
|
779
|
+
def calculate(self, req: CalculateRequest) -> CalculateResponse:
|
|
780
|
+
result = eval(req.expression, {"__builtins__": {}}, {})
|
|
781
|
+
return CalculateResponse(result=float(result))
|
|
782
|
+
|
|
783
|
+
# Run as MCP stdio server
|
|
784
|
+
if __name__ == "__main__":
|
|
785
|
+
service = MathService()
|
|
786
|
+
mcp = MCPExporter(service)
|
|
787
|
+
mcp.run_stdio()
|
|
788
|
+
```
|
|
789
|
+
|
|
790
|
+
#### Configuring MCP Clients
|
|
791
|
+
|
|
792
|
+
Any MCP-compatible client can connect to your service. For example, to configure Claude Desktop:
|
|
793
|
+
|
|
794
|
+
```json
|
|
795
|
+
{
|
|
796
|
+
"mcpServers": {
|
|
797
|
+
"my-math-service": {
|
|
798
|
+
"command": "python",
|
|
799
|
+
"args": ["/path/to/math_mcp_server.py"]
|
|
800
|
+
}
|
|
801
|
+
}
|
|
802
|
+
}
|
|
803
|
+
```
|
|
804
|
+
|
|
805
|
+
#### HTTP/ASGI Mode Example
|
|
806
|
+
|
|
807
|
+
MCP can also be mounted to existing ASGI applications:
|
|
808
|
+
|
|
809
|
+
```python
|
|
810
|
+
from pydantic_rpc import ConnecpyASGIApp
|
|
811
|
+
from pydantic_rpc.mcp import MCPExporter
|
|
812
|
+
|
|
813
|
+
# Create Connect-RPC ASGI app
|
|
814
|
+
app = ConnecpyASGIApp()
|
|
815
|
+
app.mount(MathService())
|
|
816
|
+
|
|
817
|
+
# Add MCP support via HTTP/SSE
|
|
818
|
+
mcp = MCPExporter(MathService())
|
|
819
|
+
mcp.mount_to_asgi(app, path="/mcp")
|
|
820
|
+
|
|
821
|
+
# Run with uvicorn
|
|
822
|
+
import uvicorn
|
|
823
|
+
uvicorn.run(app, host="127.0.0.1", port=8000)
|
|
824
|
+
```
|
|
825
|
+
|
|
826
|
+
MCP endpoints will be available at:
|
|
827
|
+
- SSE: `GET http://localhost:8000/mcp/sse`
|
|
828
|
+
- Messages: `POST http://localhost:8000/mcp/messages/`
|
|
829
|
+
|
|
721
830
|
### 🗄️ Protobuf file and code (Python files) generation using CLI
|
|
722
831
|
|
|
723
832
|
You can genereate protobuf files and code for a given module and a specified class using `pydantic-rpc` CLI command:
|
|
@@ -747,16 +856,61 @@ Using this generated proto file and tools as `protoc`, `buf` and `BSR`, you coul
|
|
|
747
856
|
| subclass of pydantic.BaseModel | message |
|
|
748
857
|
|
|
749
858
|
|
|
859
|
+
## ⚠️ Known Limitations
|
|
860
|
+
|
|
861
|
+
### Union Types with Collections
|
|
862
|
+
|
|
863
|
+
Due to protobuf's `oneof` restrictions, you cannot use `Union` types that contain `repeated` (list/tuple) or `map` (dict) fields directly. This is a limitation of the protobuf specification itself.
|
|
864
|
+
|
|
865
|
+
**❌ Not Supported:**
|
|
866
|
+
```python
|
|
867
|
+
from typing import Union, List, Dict
|
|
868
|
+
from pydantic_rpc import Message
|
|
869
|
+
|
|
870
|
+
# These will fail during proto compilation
|
|
871
|
+
class MyMessage(Message):
|
|
872
|
+
# Union with list - NOT SUPPORTED
|
|
873
|
+
field1: Union[List[int], str]
|
|
874
|
+
|
|
875
|
+
# Union with dict - NOT SUPPORTED
|
|
876
|
+
field2: Union[Dict[str, int], int]
|
|
877
|
+
|
|
878
|
+
# Union with nested collections - NOT SUPPORTED
|
|
879
|
+
field3: Union[List[Dict[str, int]], str]
|
|
880
|
+
```
|
|
881
|
+
|
|
882
|
+
**✅ Workaround - Use Message Wrappers:**
|
|
883
|
+
```python
|
|
884
|
+
from typing import Union, List, Dict
|
|
885
|
+
from pydantic_rpc import Message
|
|
886
|
+
|
|
887
|
+
# Wrap collections in Message types
|
|
888
|
+
class IntList(Message):
|
|
889
|
+
values: List[int]
|
|
890
|
+
|
|
891
|
+
class StringIntMap(Message):
|
|
892
|
+
values: Dict[str, int]
|
|
893
|
+
|
|
894
|
+
class MyMessage(Message):
|
|
895
|
+
# Now these work!
|
|
896
|
+
field1: Union[IntList, str]
|
|
897
|
+
field2: Union[StringIntMap, int]
|
|
898
|
+
```
|
|
899
|
+
|
|
900
|
+
This approach works because protobuf allows message types within `oneof` fields, and the collections are contained within those messages.
|
|
901
|
+
|
|
902
|
+
|
|
750
903
|
## TODO
|
|
751
|
-
- [
|
|
904
|
+
- [x] Streaming Support
|
|
752
905
|
- [x] unary-stream
|
|
753
|
-
- [
|
|
754
|
-
- [
|
|
906
|
+
- [x] stream-unary
|
|
907
|
+
- [x] stream-stream
|
|
755
908
|
- [ ] Betterproto Support
|
|
756
909
|
- [ ] Sonora-connect Support
|
|
757
910
|
- [ ] Custom Health Check Support
|
|
911
|
+
- [x] MCP (Model Context Protocol) Support via official MCP SDK
|
|
758
912
|
- [ ] Add more examples
|
|
759
|
-
- [
|
|
913
|
+
- [x] Add tests
|
|
760
914
|
|
|
761
915
|
## 📜 License
|
|
762
916
|
|
|
@@ -1,18 +1,3 @@
|
|
|
1
|
-
Metadata-Version: 2.4
|
|
2
|
-
Name: pydantic-rpc
|
|
3
|
-
Version: 0.6.1
|
|
4
|
-
Summary: A Python library for building gRPC/ConnectRPC services with Pydantic models.
|
|
5
|
-
Author: Yasushi Itoh
|
|
6
|
-
License-File: LICENSE
|
|
7
|
-
Requires-Python: >=3.11
|
|
8
|
-
Requires-Dist: connecpy>=1.4.1
|
|
9
|
-
Requires-Dist: grpcio-health-checking>=1.56.2
|
|
10
|
-
Requires-Dist: grpcio-reflection>=1.56.2
|
|
11
|
-
Requires-Dist: grpcio-tools>=1.56.2
|
|
12
|
-
Requires-Dist: pydantic>=2.1.1
|
|
13
|
-
Requires-Dist: sonora>=0.2.3
|
|
14
|
-
Description-Content-Type: text/markdown
|
|
15
|
-
|
|
16
1
|
# 🚀 PydanticRPC
|
|
17
2
|
|
|
18
3
|
**PydanticRPC** is a Python library that enables you to rapidly expose [Pydantic](https://docs.pydantic.dev/) models via [gRPC](https://grpc.io/)/[Connect RPC](https://connectrpc.com/docs/protocol/) services without writing any protobuf files. Instead, it automatically generates protobuf files on the fly from the method signatures of your Python objects and the type signatures of your Pydantic models.
|
|
@@ -125,6 +110,7 @@ app.mount(OlympicsLocationAgent())
|
|
|
125
110
|
- **For Connect-RPC:**
|
|
126
111
|
- 🌐 **Connecpy Support:** Partially supports Connect-RPC via `Connecpy`.
|
|
127
112
|
- 🛠️ **Pre-generated Protobuf Files and Code:** Pre-generate proto files and corresponding code via the CLI. By setting the environment variable (PYDANTIC_RPC_SKIP_GENERATION), you can skip runtime generation.
|
|
113
|
+
- 🤖 **MCP (Model Context Protocol) Support:** Expose your services as tools for AI assistants using the official MCP SDK, supporting both stdio and HTTP/SSE transports.
|
|
128
114
|
|
|
129
115
|
## 📦 Installation
|
|
130
116
|
|
|
@@ -178,12 +164,18 @@ class Greeter:
|
|
|
178
164
|
return HelloReply(message=f"Hello, {request.name}!")
|
|
179
165
|
|
|
180
166
|
|
|
167
|
+
async def main():
|
|
168
|
+
# You can specify a custom port (default is 50051)
|
|
169
|
+
server = AsyncIOServer(port=50052)
|
|
170
|
+
await server.run(Greeter())
|
|
171
|
+
|
|
172
|
+
|
|
181
173
|
if __name__ == "__main__":
|
|
182
|
-
|
|
183
|
-
loop = asyncio.get_event_loop()
|
|
184
|
-
loop.run_until_complete(server.run(Greeter()))
|
|
174
|
+
asyncio.run(main())
|
|
185
175
|
```
|
|
186
176
|
|
|
177
|
+
The AsyncIOServer automatically handles graceful shutdown on SIGTERM and SIGINT signals.
|
|
178
|
+
|
|
187
179
|
### 🌐 ASGI Application Example
|
|
188
180
|
|
|
189
181
|
```python
|
|
@@ -263,7 +255,7 @@ This will launch a Connecpy-based ASGI application that uses the same Pydantic m
|
|
|
263
255
|
> - Please follow the instruction described in https://go.dev/doc/install.
|
|
264
256
|
> 2. Install `protoc-gen-connecpy`:
|
|
265
257
|
> ```bash
|
|
266
|
-
> go install github.com/connecpy/protoc-gen-connecpy@latest
|
|
258
|
+
> go install github.com/i2y/connecpy/v2/protoc-gen-connecpy@latest
|
|
267
259
|
> ```
|
|
268
260
|
|
|
269
261
|
## ♻️ Skipping Protobuf Generation
|
|
@@ -275,6 +267,21 @@ export PYDANTIC_RPC_SKIP_GENERATION=true
|
|
|
275
267
|
|
|
276
268
|
When this variable is set to "true", PydanticRPC will load existing pre-generated modules rather than generating them on the fly.
|
|
277
269
|
|
|
270
|
+
## 🪧 Setting Protobuf and Connecpy/gRPC generation directory
|
|
271
|
+
By default your files will be generated in the current working directory where you ran the code from, but you can set a custom specific directory by setting the environment variable below:
|
|
272
|
+
|
|
273
|
+
```bash
|
|
274
|
+
export PYDANTIC_RPC_PROTO_PATH=/your/path
|
|
275
|
+
```
|
|
276
|
+
|
|
277
|
+
## ⚠️ Reserved Fields
|
|
278
|
+
|
|
279
|
+
You can also set an environment variable to reserve a set number of fields for proto generation, for backward and forward compatibility.
|
|
280
|
+
|
|
281
|
+
```bash
|
|
282
|
+
export PYDANTIC_RPC_RESERVED_FIELDS=1
|
|
283
|
+
```
|
|
284
|
+
|
|
278
285
|
## 💎 Advanced Features
|
|
279
286
|
|
|
280
287
|
### 🌊 Response Streaming
|
|
@@ -733,6 +740,74 @@ if __name__ == "__main__":
|
|
|
733
740
|
|
|
734
741
|
TODO
|
|
735
742
|
|
|
743
|
+
### 🤖 MCP (Model Context Protocol) Support
|
|
744
|
+
|
|
745
|
+
PydanticRPC can expose your services as MCP tools for AI assistants using FastMCP. This enables seamless integration with any MCP-compatible client.
|
|
746
|
+
|
|
747
|
+
#### Stdio Mode Example
|
|
748
|
+
|
|
749
|
+
```python
|
|
750
|
+
from pydantic_rpc import Message
|
|
751
|
+
from pydantic_rpc.mcp import MCPExporter
|
|
752
|
+
|
|
753
|
+
class CalculateRequest(Message):
|
|
754
|
+
expression: str
|
|
755
|
+
|
|
756
|
+
class CalculateResponse(Message):
|
|
757
|
+
result: float
|
|
758
|
+
|
|
759
|
+
class MathService:
|
|
760
|
+
def calculate(self, req: CalculateRequest) -> CalculateResponse:
|
|
761
|
+
result = eval(req.expression, {"__builtins__": {}}, {})
|
|
762
|
+
return CalculateResponse(result=float(result))
|
|
763
|
+
|
|
764
|
+
# Run as MCP stdio server
|
|
765
|
+
if __name__ == "__main__":
|
|
766
|
+
service = MathService()
|
|
767
|
+
mcp = MCPExporter(service)
|
|
768
|
+
mcp.run_stdio()
|
|
769
|
+
```
|
|
770
|
+
|
|
771
|
+
#### Configuring MCP Clients
|
|
772
|
+
|
|
773
|
+
Any MCP-compatible client can connect to your service. For example, to configure Claude Desktop:
|
|
774
|
+
|
|
775
|
+
```json
|
|
776
|
+
{
|
|
777
|
+
"mcpServers": {
|
|
778
|
+
"my-math-service": {
|
|
779
|
+
"command": "python",
|
|
780
|
+
"args": ["/path/to/math_mcp_server.py"]
|
|
781
|
+
}
|
|
782
|
+
}
|
|
783
|
+
}
|
|
784
|
+
```
|
|
785
|
+
|
|
786
|
+
#### HTTP/ASGI Mode Example
|
|
787
|
+
|
|
788
|
+
MCP can also be mounted to existing ASGI applications:
|
|
789
|
+
|
|
790
|
+
```python
|
|
791
|
+
from pydantic_rpc import ConnecpyASGIApp
|
|
792
|
+
from pydantic_rpc.mcp import MCPExporter
|
|
793
|
+
|
|
794
|
+
# Create Connect-RPC ASGI app
|
|
795
|
+
app = ConnecpyASGIApp()
|
|
796
|
+
app.mount(MathService())
|
|
797
|
+
|
|
798
|
+
# Add MCP support via HTTP/SSE
|
|
799
|
+
mcp = MCPExporter(MathService())
|
|
800
|
+
mcp.mount_to_asgi(app, path="/mcp")
|
|
801
|
+
|
|
802
|
+
# Run with uvicorn
|
|
803
|
+
import uvicorn
|
|
804
|
+
uvicorn.run(app, host="127.0.0.1", port=8000)
|
|
805
|
+
```
|
|
806
|
+
|
|
807
|
+
MCP endpoints will be available at:
|
|
808
|
+
- SSE: `GET http://localhost:8000/mcp/sse`
|
|
809
|
+
- Messages: `POST http://localhost:8000/mcp/messages/`
|
|
810
|
+
|
|
736
811
|
### 🗄️ Protobuf file and code (Python files) generation using CLI
|
|
737
812
|
|
|
738
813
|
You can genereate protobuf files and code for a given module and a specified class using `pydantic-rpc` CLI command:
|
|
@@ -762,16 +837,61 @@ Using this generated proto file and tools as `protoc`, `buf` and `BSR`, you coul
|
|
|
762
837
|
| subclass of pydantic.BaseModel | message |
|
|
763
838
|
|
|
764
839
|
|
|
840
|
+
## ⚠️ Known Limitations
|
|
841
|
+
|
|
842
|
+
### Union Types with Collections
|
|
843
|
+
|
|
844
|
+
Due to protobuf's `oneof` restrictions, you cannot use `Union` types that contain `repeated` (list/tuple) or `map` (dict) fields directly. This is a limitation of the protobuf specification itself.
|
|
845
|
+
|
|
846
|
+
**❌ Not Supported:**
|
|
847
|
+
```python
|
|
848
|
+
from typing import Union, List, Dict
|
|
849
|
+
from pydantic_rpc import Message
|
|
850
|
+
|
|
851
|
+
# These will fail during proto compilation
|
|
852
|
+
class MyMessage(Message):
|
|
853
|
+
# Union with list - NOT SUPPORTED
|
|
854
|
+
field1: Union[List[int], str]
|
|
855
|
+
|
|
856
|
+
# Union with dict - NOT SUPPORTED
|
|
857
|
+
field2: Union[Dict[str, int], int]
|
|
858
|
+
|
|
859
|
+
# Union with nested collections - NOT SUPPORTED
|
|
860
|
+
field3: Union[List[Dict[str, int]], str]
|
|
861
|
+
```
|
|
862
|
+
|
|
863
|
+
**✅ Workaround - Use Message Wrappers:**
|
|
864
|
+
```python
|
|
865
|
+
from typing import Union, List, Dict
|
|
866
|
+
from pydantic_rpc import Message
|
|
867
|
+
|
|
868
|
+
# Wrap collections in Message types
|
|
869
|
+
class IntList(Message):
|
|
870
|
+
values: List[int]
|
|
871
|
+
|
|
872
|
+
class StringIntMap(Message):
|
|
873
|
+
values: Dict[str, int]
|
|
874
|
+
|
|
875
|
+
class MyMessage(Message):
|
|
876
|
+
# Now these work!
|
|
877
|
+
field1: Union[IntList, str]
|
|
878
|
+
field2: Union[StringIntMap, int]
|
|
879
|
+
```
|
|
880
|
+
|
|
881
|
+
This approach works because protobuf allows message types within `oneof` fields, and the collections are contained within those messages.
|
|
882
|
+
|
|
883
|
+
|
|
765
884
|
## TODO
|
|
766
|
-
- [
|
|
885
|
+
- [x] Streaming Support
|
|
767
886
|
- [x] unary-stream
|
|
768
|
-
- [
|
|
769
|
-
- [
|
|
887
|
+
- [x] stream-unary
|
|
888
|
+
- [x] stream-stream
|
|
770
889
|
- [ ] Betterproto Support
|
|
771
890
|
- [ ] Sonora-connect Support
|
|
772
891
|
- [ ] Custom Health Check Support
|
|
892
|
+
- [x] MCP (Model Context Protocol) Support via official MCP SDK
|
|
773
893
|
- [ ] Add more examples
|
|
774
|
-
- [
|
|
894
|
+
- [x] Add tests
|
|
775
895
|
|
|
776
896
|
## 📜 License
|
|
777
897
|
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
# MCP (Model Context Protocol) Support
|
|
2
|
+
|
|
3
|
+
pydantic-rpc provides built-in support for exposing your services as MCP servers, allowing them to be used as tools by AI assistants.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- **Automatic tool generation** from pydantic-rpc service methods
|
|
8
|
+
- **Type-safe** parameter and return value handling using Pydantic models
|
|
9
|
+
- **Multiple transport options**:
|
|
10
|
+
- stdio (for Claude Desktop and other MCP clients)
|
|
11
|
+
- HTTP/SSE (for web-based integrations)
|
|
12
|
+
- **Async and sync** method support
|
|
13
|
+
|
|
14
|
+
## Quick Start
|
|
15
|
+
|
|
16
|
+
### 1. Define your service
|
|
17
|
+
|
|
18
|
+
```python
|
|
19
|
+
from pydantic_rpc import Message
|
|
20
|
+
from pydantic_rpc.mcp import MCPExporter
|
|
21
|
+
|
|
22
|
+
class CalculateRequest(Message):
|
|
23
|
+
expression: str
|
|
24
|
+
|
|
25
|
+
class CalculateResponse(Message):
|
|
26
|
+
result: float
|
|
27
|
+
|
|
28
|
+
class CalculatorService:
|
|
29
|
+
def calculate(self, request: CalculateRequest) -> CalculateResponse:
|
|
30
|
+
result = eval(request.expression, {"__builtins__": {}}, {})
|
|
31
|
+
return CalculateResponse(result=float(result))
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
### 2. Export as MCP server
|
|
35
|
+
|
|
36
|
+
```python
|
|
37
|
+
if __name__ == "__main__":
|
|
38
|
+
service = CalculatorService()
|
|
39
|
+
mcp = MCPExporter(service, name="Calculator")
|
|
40
|
+
mcp.run_stdio() # For stdio transport
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
### 3. Use with MCP clients
|
|
44
|
+
|
|
45
|
+
Configure your MCP client to run the Python script. The service methods will be automatically exposed as tools.
|
|
46
|
+
|
|
47
|
+
## Transport Options
|
|
48
|
+
|
|
49
|
+
### stdio Transport
|
|
50
|
+
|
|
51
|
+
For use with Claude Desktop and other stdio-based MCP clients:
|
|
52
|
+
|
|
53
|
+
```python
|
|
54
|
+
mcp = MCPExporter(service)
|
|
55
|
+
mcp.run_stdio()
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
### HTTP/SSE Transport
|
|
59
|
+
|
|
60
|
+
For web-based integrations:
|
|
61
|
+
|
|
62
|
+
```python
|
|
63
|
+
# Standalone HTTP server
|
|
64
|
+
app = mcp.get_asgi_app()
|
|
65
|
+
# Run with uvicorn or similar ASGI server
|
|
66
|
+
|
|
67
|
+
# Mount to existing ASGI app
|
|
68
|
+
from pydantic_rpc import ConnecpyASGIApp
|
|
69
|
+
connect_app = ConnecpyASGIApp()
|
|
70
|
+
connect_app.mount(service)
|
|
71
|
+
mcp.mount_to_asgi(connect_app, path="/mcp")
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
## Examples
|
|
75
|
+
|
|
76
|
+
See the `examples/` directory for complete examples:
|
|
77
|
+
- `mcp_example.py` - Calculator service with stdio transport
|
|
78
|
+
- `mcp_http_example.py` - HTTP/SSE transport with Connect-RPC integration
|
|
79
|
+
- `mcp_simple_calculator.py` - Minimal example
|
|
80
|
+
|
|
81
|
+
## How It Works
|
|
82
|
+
|
|
83
|
+
1. MCPExporter introspects your service class to find public methods
|
|
84
|
+
2. For each method, it extracts parameter and return type information
|
|
85
|
+
3. Pydantic models are converted to JSON Schema for tool descriptions
|
|
86
|
+
4. Methods are wrapped to handle MCP protocol messages
|
|
87
|
+
5. The resulting tools can be called by MCP clients
|
|
88
|
+
|
|
89
|
+
## Requirements
|
|
90
|
+
|
|
91
|
+
```bash
|
|
92
|
+
pip install pydantic-rpc[mcp]
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
This installs the official MCP SDK and required dependencies.
|
|
@@ -1,22 +1,22 @@
|
|
|
1
|
-
import asyncio
|
|
2
|
-
|
|
3
|
-
from pydantic_rpc import AsyncIOServer, Message
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
class HelloRequest(Message):
|
|
7
|
-
name: str
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
class HelloReply(Message):
|
|
11
|
-
message: str
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
class Greeter:
|
|
15
|
-
async def say_hello(self, request: HelloRequest) -> HelloReply:
|
|
16
|
-
return HelloReply(message=f"Hello, {request.name}!")
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
if __name__ == "__main__":
|
|
20
|
-
s = AsyncIOServer()
|
|
21
|
-
loop = asyncio.get_event_loop()
|
|
22
|
-
loop.run_until_complete(s.run(Greeter()))
|
|
1
|
+
import asyncio
|
|
2
|
+
|
|
3
|
+
from pydantic_rpc import AsyncIOServer, Message
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class HelloRequest(Message):
|
|
7
|
+
name: str
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class HelloReply(Message):
|
|
11
|
+
message: str
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class Greeter:
|
|
15
|
+
async def say_hello(self, request: HelloRequest) -> HelloReply:
|
|
16
|
+
return HelloReply(message=f"Hello, {request.name}!")
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
if __name__ == "__main__":
|
|
20
|
+
s = AsyncIOServer()
|
|
21
|
+
loop = asyncio.get_event_loop()
|
|
22
|
+
loop.run_until_complete(s.run(Greeter()))
|