pydantic-rpc 0.6.0__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.
Files changed (82) hide show
  1. pydantic_rpc-0.7.0/.github/workflows/release.yml +35 -0
  2. pydantic_rpc-0.7.0/.github/workflows/test.yml +47 -0
  3. pydantic_rpc-0.7.0/.gitignore +24 -0
  4. {pydantic_rpc-0.6.0 → pydantic_rpc-0.7.0}/PKG-INFO +475 -13
  5. pydantic_rpc-0.7.0/README.md +898 -0
  6. pydantic_rpc-0.7.0/docs/mcp.md +95 -0
  7. {pydantic_rpc-0.6.0 → pydantic_rpc-0.7.0}/examples/agent_aio_grpc.py +11 -8
  8. {pydantic_rpc-0.6.0 → pydantic_rpc-0.7.0}/examples/agent_connecpy.py +11 -8
  9. {pydantic_rpc-0.6.0 → pydantic_rpc-0.7.0}/examples/asyncio_greeting.py +22 -22
  10. {pydantic_rpc-0.6.0 → pydantic_rpc-0.7.0}/examples/barservice.proto +17 -17
  11. {pydantic_rpc-0.6.0 → pydantic_rpc-0.7.0}/examples/foobar.py +76 -76
  12. {pydantic_rpc-0.6.0 → pydantic_rpc-0.7.0}/examples/foobar_client.py +20 -20
  13. {pydantic_rpc-0.6.0 → pydantic_rpc-0.7.0}/examples/fooservice.proto +21 -21
  14. {pydantic_rpc-0.6.0 → pydantic_rpc-0.7.0}/examples/google/protobuf/duration.proto +115 -115
  15. {pydantic_rpc-0.6.0 → pydantic_rpc-0.7.0}/examples/google/protobuf/timestamp.proto +144 -144
  16. {pydantic_rpc-0.6.0 → pydantic_rpc-0.7.0}/examples/greeter.proto +35 -35
  17. pydantic_rpc-0.7.0/examples/greeter_connecpy.py +124 -0
  18. {pydantic_rpc-0.6.0 → pydantic_rpc-0.7.0}/examples/greeter_connecpy_client.py +10 -9
  19. {pydantic_rpc-0.6.0 → pydantic_rpc-0.7.0}/examples/greeter_pb2_grpc.py +4 -17
  20. {pydantic_rpc-0.6.0 → pydantic_rpc-0.7.0}/examples/greeting.py +45 -45
  21. {pydantic_rpc-0.6.0 → pydantic_rpc-0.7.0}/examples/greeting_asgi.py +55 -55
  22. {pydantic_rpc-0.6.0 → pydantic_rpc-0.7.0}/examples/greeting_connecpy.py +44 -46
  23. {pydantic_rpc-0.6.0 → pydantic_rpc-0.7.0}/examples/greeting_wsgi.py +63 -63
  24. pydantic_rpc-0.7.0/examples/mcp_debug_example.py +74 -0
  25. pydantic_rpc-0.7.0/examples/mcp_example.py +129 -0
  26. pydantic_rpc-0.7.0/examples/mcp_http_example.py +125 -0
  27. pydantic_rpc-0.7.0/examples/mcp_simple_calculator.py +45 -0
  28. {pydantic_rpc-0.6.0 → pydantic_rpc-0.7.0}/examples/olympicsagent.proto +40 -40
  29. {pydantic_rpc-0.6.0 → pydantic_rpc-0.7.0}/examples/olympicslocationagent.proto +24 -24
  30. {pydantic_rpc-0.6.0 → pydantic_rpc-0.7.0}/examples/olympicslocationagent_connecpy.py +30 -0
  31. {pydantic_rpc-0.6.0 → pydantic_rpc-0.7.0}/pyproject.toml +9 -3
  32. {pydantic_rpc-0.6.0 → pydantic_rpc-0.7.0}/src/pydantic_rpc/__init__.py +10 -0
  33. pydantic_rpc-0.7.0/src/pydantic_rpc/core.py +2445 -0
  34. pydantic_rpc-0.7.0/src/pydantic_rpc/mcp/__init__.py +5 -0
  35. pydantic_rpc-0.7.0/src/pydantic_rpc/mcp/converter.py +115 -0
  36. pydantic_rpc-0.7.0/src/pydantic_rpc/mcp/exporter.py +283 -0
  37. pydantic_rpc-0.7.0/tests/google_protobuf/greeterwithduration.proto +14 -0
  38. pydantic_rpc-0.7.0/tests/google_protobuf/greeterwithtimestamp.proto +14 -0
  39. pydantic_rpc-0.7.0/tests/google_protobuf/test_google_protobuf.py +41 -0
  40. pydantic_rpc-0.7.0/tests/greeterwithduration.proto +14 -0
  41. pydantic_rpc-0.7.0/tests/greeterwithtimestamp.proto +14 -0
  42. pydantic_rpc-0.7.0/tests/test_apps.py +378 -0
  43. pydantic_rpc-0.7.0/tests/test_conversion.py +1126 -0
  44. pydantic_rpc-0.7.0/tests/test_mcp.py +181 -0
  45. pydantic_rpc-0.7.0/tests/test_utils.py +511 -0
  46. {pydantic_rpc-0.6.0 → pydantic_rpc-0.7.0}/uv.lock +764 -852
  47. pydantic_rpc-0.6.0/.gitignore +0 -13
  48. pydantic_rpc-0.6.0/README.md +0 -440
  49. pydantic_rpc-0.6.0/examples/greeter_connecpy.py +0 -105
  50. pydantic_rpc-0.6.0/src/pydantic_rpc/core.py +0 -1455
  51. pydantic_rpc-0.6.0/tests/asyncechoservice_connecpy.py +0 -101
  52. pydantic_rpc-0.6.0/tests/asyncechoservice_pb2.py +0 -40
  53. pydantic_rpc-0.6.0/tests/asyncechoservice_pb2.pyi +0 -17
  54. pydantic_rpc-0.6.0/tests/asyncechoservice_pb2_grpc.py +0 -110
  55. pydantic_rpc-0.6.0/tests/echoservice_connecpy.py +0 -101
  56. pydantic_rpc-0.6.0/tests/echoservice_pb2.py +0 -40
  57. pydantic_rpc-0.6.0/tests/echoservice_pb2.pyi +0 -17
  58. pydantic_rpc-0.6.0/tests/echoservice_pb2_grpc.py +0 -110
  59. pydantic_rpc-0.6.0/tests/test_apps.py +0 -190
  60. {pydantic_rpc-0.6.0 → pydantic_rpc-0.7.0}/.python-version +0 -0
  61. {pydantic_rpc-0.6.0 → pydantic_rpc-0.7.0}/LICENSE +0 -0
  62. {pydantic_rpc-0.6.0 → pydantic_rpc-0.7.0}/examples/README.md +0 -0
  63. {pydantic_rpc-0.6.0 → pydantic_rpc-0.7.0}/examples/barservice_pb2.py +0 -0
  64. {pydantic_rpc-0.6.0 → pydantic_rpc-0.7.0}/examples/barservice_pb2.pyi +0 -0
  65. {pydantic_rpc-0.6.0 → pydantic_rpc-0.7.0}/examples/barservice_pb2_grpc.py +0 -0
  66. {pydantic_rpc-0.6.0 → pydantic_rpc-0.7.0}/examples/fooservice_pb2.py +0 -0
  67. {pydantic_rpc-0.6.0 → pydantic_rpc-0.7.0}/examples/fooservice_pb2.pyi +0 -0
  68. {pydantic_rpc-0.6.0 → pydantic_rpc-0.7.0}/examples/fooservice_pb2_grpc.py +0 -0
  69. {pydantic_rpc-0.6.0 → pydantic_rpc-0.7.0}/examples/greeter_client.py +0 -0
  70. {pydantic_rpc-0.6.0 → pydantic_rpc-0.7.0}/examples/greeter_pb2.py +0 -0
  71. {pydantic_rpc-0.6.0 → pydantic_rpc-0.7.0}/examples/greeter_pb2.pyi +0 -0
  72. {pydantic_rpc-0.6.0 → pydantic_rpc-0.7.0}/examples/greeter_sonora_client.py +0 -0
  73. {pydantic_rpc-0.6.0 → pydantic_rpc-0.7.0}/examples/greeting_using_exsiting_pb2_modules.py +0 -0
  74. {pydantic_rpc-0.6.0 → pydantic_rpc-0.7.0}/examples/olympicsagent_pb2.py +0 -0
  75. {pydantic_rpc-0.6.0 → pydantic_rpc-0.7.0}/examples/olympicsagent_pb2.pyi +0 -0
  76. {pydantic_rpc-0.6.0 → pydantic_rpc-0.7.0}/examples/olympicsagent_pb2_grpc.py +0 -0
  77. {pydantic_rpc-0.6.0 → pydantic_rpc-0.7.0}/examples/olympicslocationagent_pb2.py +0 -0
  78. {pydantic_rpc-0.6.0 → pydantic_rpc-0.7.0}/examples/olympicslocationagent_pb2.pyi +0 -0
  79. {pydantic_rpc-0.6.0 → pydantic_rpc-0.7.0}/examples/olympicslocationagent_pb2_grpc.py +0 -0
  80. {pydantic_rpc-0.6.0 → pydantic_rpc-0.7.0}/src/pydantic_rpc/py.typed +0 -0
  81. {pydantic_rpc-0.6.0 → pydantic_rpc-0.7.0}/tests/asyncechoservice.proto +0 -0
  82. {pydantic_rpc-0.6.0 → pydantic_rpc-0.7.0}/tests/echoservice.proto +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,16 +1,20 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pydantic-rpc
3
- Version: 0.6.0
3
+ Version: 0.7.0
4
4
  Summary: A Python library for building gRPC/ConnectRPC services with Pydantic models.
5
5
  Author: Yasushi Itoh
6
6
  License-File: LICENSE
7
7
  Requires-Python: >=3.11
8
- Requires-Dist: connecpy>=1.3.2
8
+ Requires-Dist: annotated-types>=0.5.0
9
+ Requires-Dist: connecpy==2.0.0
9
10
  Requires-Dist: grpcio-health-checking>=1.56.2
10
11
  Requires-Dist: grpcio-reflection>=1.56.2
11
12
  Requires-Dist: grpcio-tools>=1.56.2
13
+ Requires-Dist: grpcio>=1.56.2
14
+ Requires-Dist: mcp>=1.9.4
12
15
  Requires-Dist: pydantic>=2.1.1
13
16
  Requires-Dist: sonora>=0.2.3
17
+ Requires-Dist: starlette>=0.27.0
14
18
  Description-Content-Type: text/markdown
15
19
 
16
20
  # 🚀 PydanticRPC
@@ -23,7 +27,9 @@ Below is an example of a simple gRPC service that exposes a [PydanticAI](https:/
23
27
  ```python
24
28
  import asyncio
25
29
 
30
+ from openai import AsyncOpenAI
26
31
  from pydantic_ai import Agent
32
+ from pydantic_ai.models.openai import OpenAIModel
27
33
  from pydantic_rpc import AsyncIOServer, Message
28
34
 
29
35
 
@@ -42,7 +48,15 @@ class Olympics(Message):
42
48
 
43
49
  class OlympicsLocationAgent:
44
50
  def __init__(self):
45
- self._agent = Agent("ollama:llama3.2", result_type=CityLocation)
51
+ client = AsyncOpenAI(
52
+ base_url="http://localhost:11434/v1",
53
+ api_key="ollama_api_key",
54
+ )
55
+ ollama_model = OpenAIModel(
56
+ model_name="llama3.2",
57
+ openai_client=client,
58
+ )
59
+ self._agent = Agent(ollama_model)
46
60
 
47
61
  async def ask(self, req: Olympics) -> CityLocation:
48
62
  result = await self._agent.run(req.prompt())
@@ -60,7 +74,9 @@ And here is an example of a simple Connect RPC service that exposes the same age
60
74
  ```python
61
75
  import asyncio
62
76
 
77
+ from openai import AsyncOpenAI
63
78
  from pydantic_ai import Agent
79
+ from pydantic_ai.models.openai import OpenAIModel
64
80
  from pydantic_rpc import ConnecpyASGIApp, Message
65
81
 
66
82
 
@@ -78,7 +94,15 @@ class Olympics(Message):
78
94
 
79
95
  class OlympicsLocationAgent:
80
96
  def __init__(self):
81
- self._agent = Agent("ollama:llama3.2", result_type=CityLocation)
97
+ client = AsyncOpenAI(
98
+ base_url="http://localhost:11434/v1",
99
+ api_key="ollama_api_key",
100
+ )
101
+ ollama_model = OpenAIModel(
102
+ model_name="llama3.2",
103
+ openai_client=client,
104
+ )
105
+ self._agent = Agent(ollama_model, result_type=CityLocation)
82
106
 
83
107
  async def ask(self, req: Olympics) -> CityLocation:
84
108
  result = await self._agent.run(req.prompt())
@@ -105,6 +129,7 @@ app.mount(OlympicsLocationAgent())
105
129
  - **For Connect-RPC:**
106
130
  - 🌐 **Connecpy Support:** Partially supports Connect-RPC via `Connecpy`.
107
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.
108
133
 
109
134
  ## 📦 Installation
110
135
 
@@ -158,12 +183,18 @@ class Greeter:
158
183
  return HelloReply(message=f"Hello, {request.name}!")
159
184
 
160
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
+
161
192
  if __name__ == "__main__":
162
- server = AsyncIOServer()
163
- loop = asyncio.get_event_loop()
164
- loop.run_until_complete(server.run(Greeter()))
193
+ asyncio.run(main())
165
194
  ```
166
195
 
196
+ The AsyncIOServer automatically handles graceful shutdown on SIGTERM and SIGINT signals.
197
+
167
198
  ### 🌐 ASGI Application Example
168
199
 
169
200
  ```python
@@ -243,7 +274,7 @@ This will launch a Connecpy-based ASGI application that uses the same Pydantic m
243
274
  > - Please follow the instruction described in https://go.dev/doc/install.
244
275
  > 2. Install `protoc-gen-connecpy`:
245
276
  > ```bash
246
- > go install github.com/connecpy/protoc-gen-connecpy@latest
277
+ > go install github.com/i2y/connecpy/v2/protoc-gen-connecpy@latest
247
278
  > ```
248
279
 
249
280
  ## ♻️ Skipping Protobuf Generation
@@ -255,6 +286,21 @@ export PYDANTIC_RPC_SKIP_GENERATION=true
255
286
 
256
287
  When this variable is set to "true", PydanticRPC will load existing pre-generated modules rather than generating them on the fly.
257
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
+
258
304
  ## 💎 Advanced Features
259
305
 
260
306
  ### 🌊 Response Streaming
@@ -268,8 +314,10 @@ Please see the sample code below:
268
314
  import asyncio
269
315
  from typing import Annotated, AsyncIterator
270
316
 
317
+ from openai import AsyncOpenAI
271
318
  from pydantic import Field
272
319
  from pydantic_ai import Agent
320
+ from pydantic_ai.models.openai import OpenAIModel
273
321
  from pydantic_rpc import AsyncIOServer, Message
274
322
 
275
323
 
@@ -302,7 +350,15 @@ class StreamingResult(Message):
302
350
 
303
351
  class OlympicsAgent:
304
352
  def __init__(self):
305
- self._agent = Agent("ollama:llama3.2")
353
+ client = AsyncOpenAI(
354
+ base_url='http://localhost:11434/v1',
355
+ api_key='ollama_api_key',
356
+ )
357
+ ollama_model = OpenAIModel(
358
+ model_name='llama3.2',
359
+ openai_client=client,
360
+ )
361
+ self._agent = Agent(ollama_model)
306
362
 
307
363
  async def ask(self, req: OlympicsQuery) -> CityLocation:
308
364
  result = await self._agent.run(req.prompt(), result_type=CityLocation)
@@ -324,6 +380,299 @@ if __name__ == "__main__":
324
380
 
325
381
  In the example above, the `ask_stream` method returns an `AsyncIterator[StreamingResult]` object, which is considered a streaming method. The `StreamingResult` class is a Pydantic model that defines the response type of the streaming method. You can use any Pydantic model as the response type.
326
382
 
383
+ Now, you can call the `ask_stream` method of the server described above using your preferred gRPC client tool. The example below uses `buf curl`.
384
+
385
+
386
+ ```console
387
+ % buf curl --data '{"start": 1980, "end": 2024}' -v http://localhost:50051/olympicsagent.v1.OlympicsAgent/AskStream --protocol grpc --http2-prior-knowledge
388
+
389
+ buf: * Using server reflection to resolve "olympicsagent.v1.OlympicsAgent"
390
+ buf: * Dialing (tcp) localhost:50051...
391
+ buf: * Connected to [::1]:50051
392
+ buf: > (#1) POST /grpc.reflection.v1.ServerReflection/ServerReflectionInfo
393
+ buf: > (#1) Accept-Encoding: identity
394
+ buf: > (#1) Content-Type: application/grpc+proto
395
+ buf: > (#1) Grpc-Accept-Encoding: gzip
396
+ buf: > (#1) Grpc-Timeout: 119997m
397
+ buf: > (#1) Te: trailers
398
+ buf: > (#1) User-Agent: grpc-go-connect/1.12.0 (go1.21.4) buf/1.28.1
399
+ buf: > (#1)
400
+ buf: } (#1) [5 bytes data]
401
+ buf: } (#1) [32 bytes data]
402
+ buf: < (#1) HTTP/2.0 200 OK
403
+ buf: < (#1) Content-Type: application/grpc
404
+ buf: < (#1) Grpc-Message: Method not found!
405
+ buf: < (#1) Grpc-Status: 12
406
+ buf: < (#1)
407
+ buf: * (#1) Call complete
408
+ buf: > (#2) POST /grpc.reflection.v1alpha.ServerReflection/ServerReflectionInfo
409
+ buf: > (#2) Accept-Encoding: identity
410
+ buf: > (#2) Content-Type: application/grpc+proto
411
+ buf: > (#2) Grpc-Accept-Encoding: gzip
412
+ buf: > (#2) Grpc-Timeout: 119967m
413
+ buf: > (#2) Te: trailers
414
+ buf: > (#2) User-Agent: grpc-go-connect/1.12.0 (go1.21.4) buf/1.28.1
415
+ buf: > (#2)
416
+ buf: } (#2) [5 bytes data]
417
+ buf: } (#2) [32 bytes data]
418
+ buf: < (#2) HTTP/2.0 200 OK
419
+ buf: < (#2) Content-Type: application/grpc
420
+ buf: < (#2) Grpc-Accept-Encoding: identity, deflate, gzip
421
+ buf: < (#2)
422
+ buf: { (#2) [5 bytes data]
423
+ buf: { (#2) [434 bytes data]
424
+ buf: * Server reflection has resolved file "olympicsagent.proto"
425
+ buf: * Invoking RPC olympicsagent.v1.OlympicsAgent.AskStream
426
+ buf: > (#3) POST /olympicsagent.v1.OlympicsAgent/AskStream
427
+ buf: > (#3) Accept-Encoding: identity
428
+ buf: > (#3) Content-Type: application/grpc+proto
429
+ buf: > (#3) Grpc-Accept-Encoding: gzip
430
+ buf: > (#3) Grpc-Timeout: 119947m
431
+ buf: > (#3) Te: trailers
432
+ buf: > (#3) User-Agent: grpc-go-connect/1.12.0 (go1.21.4) buf/1.28.1
433
+ buf: > (#3)
434
+ buf: } (#3) [5 bytes data]
435
+ buf: } (#3) [6 bytes data]
436
+ buf: * (#3) Finished upload
437
+ buf: < (#3) HTTP/2.0 200 OK
438
+ buf: < (#3) Content-Type: application/grpc
439
+ buf: < (#3) Grpc-Accept-Encoding: identity, deflate, gzip
440
+ buf: < (#3)
441
+ buf: { (#3) [5 bytes data]
442
+ buf: { (#3) [25 bytes data]
443
+ {
444
+ "answer": "Here's a list of Summer"
445
+ }
446
+ buf: { (#3) [5 bytes data]
447
+ buf: { (#3) [31 bytes data]
448
+ {
449
+ "answer": " and Winter Olympics from 198"
450
+ }
451
+ buf: { (#3) [5 bytes data]
452
+ buf: { (#3) [29 bytes data]
453
+ {
454
+ "answer": "0 to 2024:\n\nSummer Olympics"
455
+ }
456
+ buf: { (#3) [5 bytes data]
457
+ buf: { (#3) [20 bytes data]
458
+ {
459
+ "answer": ":\n1. 1980 - Moscow"
460
+ }
461
+ buf: { (#3) [5 bytes data]
462
+ buf: { (#3) [20 bytes data]
463
+ {
464
+ "answer": ", Soviet Union\n2. "
465
+ }
466
+ buf: { (#3) [5 bytes data]
467
+ buf: { (#3) [32 bytes data]
468
+ {
469
+ "answer": "1984 - Los Angeles, California"
470
+ }
471
+ buf: { (#3) [5 bytes data]
472
+ buf: { (#3) [15 bytes data]
473
+ {
474
+ "answer": ", USA\n3. 1988"
475
+ }
476
+ buf: { (#3) [5 bytes data]
477
+ buf: { (#3) [26 bytes data]
478
+ {
479
+ "answer": " - Seoul, South Korea\n4."
480
+ }
481
+ buf: { (#3) [5 bytes data]
482
+ buf: { (#3) [27 bytes data]
483
+ {
484
+ "answer": " 1992 - Barcelona, Spain\n"
485
+ }
486
+ buf: { (#3) [5 bytes data]
487
+ buf: { (#3) [20 bytes data]
488
+ {
489
+ "answer": "5. 1996 - Atlanta,"
490
+ }
491
+ buf: { (#3) [5 bytes data]
492
+ buf: { (#3) [22 bytes data]
493
+ {
494
+ "answer": " Georgia, USA\n6. 200"
495
+ }
496
+ buf: { (#3) [5 bytes data]
497
+ buf: { (#3) [26 bytes data]
498
+ {
499
+ "answer": "0 - Sydney, Australia\n7."
500
+ }
501
+ buf: { (#3) [5 bytes data]
502
+ buf: { (#3) [25 bytes data]
503
+ {
504
+ "answer": " 2004 - Athens, Greece\n"
505
+ }
506
+ buf: { (#3) [5 bytes data]
507
+ buf: { (#3) [20 bytes data]
508
+ {
509
+ "answer": "8. 2008 - Beijing,"
510
+ }
511
+ buf: { (#3) [5 bytes data]
512
+ buf: { (#3) [18 bytes data]
513
+ {
514
+ "answer": " China\n9. 2012 -"
515
+ }
516
+ buf: { (#3) [5 bytes data]
517
+ buf: { (#3) [29 bytes data]
518
+ {
519
+ "answer": " London, United Kingdom\n10."
520
+ }
521
+ buf: { (#3) [5 bytes data]
522
+ buf: { (#3) [24 bytes data]
523
+ {
524
+ "answer": " 2016 - Rio de Janeiro"
525
+ }
526
+ buf: { (#3) [5 bytes data]
527
+ buf: { (#3) [18 bytes data]
528
+ {
529
+ "answer": ", Brazil\n11. 202"
530
+ }
531
+ buf: { (#3) [5 bytes data]
532
+ buf: { (#3) [24 bytes data]
533
+ {
534
+ "answer": "0 - Tokyo, Japan (held"
535
+ }
536
+ buf: { (#3) [5 bytes data]
537
+ buf: { (#3) [21 bytes data]
538
+ {
539
+ "answer": " in 2021 due to the"
540
+ }
541
+ buf: { (#3) [5 bytes data]
542
+ buf: { (#3) [26 bytes data]
543
+ {
544
+ "answer": " COVID-19 pandemic)\n12. "
545
+ }
546
+ buf: { (#3) [5 bytes data]
547
+ buf: { (#3) [28 bytes data]
548
+ {
549
+ "answer": "2024 - Paris, France\n\nNote"
550
+ }
551
+ buf: { (#3) [5 bytes data]
552
+ buf: { (#3) [41 bytes data]
553
+ {
554
+ "answer": ": The Olympics were held without a host"
555
+ }
556
+ buf: { (#3) [5 bytes data]
557
+ buf: { (#3) [26 bytes data]
558
+ {
559
+ "answer": " city for one year (2022"
560
+ }
561
+ buf: { (#3) [5 bytes data]
562
+ buf: { (#3) [42 bytes data]
563
+ {
564
+ "answer": ", due to the Russian invasion of Ukraine"
565
+ }
566
+ buf: { (#3) [5 bytes data]
567
+ buf: { (#3) [29 bytes data]
568
+ {
569
+ "answer": ").\n\nWinter Olympics:\n1. 198"
570
+ }
571
+ buf: { (#3) [5 bytes data]
572
+ buf: { (#3) [27 bytes data]
573
+ {
574
+ "answer": "0 - Lake Placid, New York"
575
+ }
576
+ buf: { (#3) [5 bytes data]
577
+ buf: { (#3) [15 bytes data]
578
+ {
579
+ "answer": ", USA\n2. 1984"
580
+ }
581
+ buf: { (#3) [5 bytes data]
582
+ buf: { (#3) [27 bytes data]
583
+ {
584
+ "answer": " - Sarajevo, Yugoslavia ("
585
+ }
586
+ buf: { (#3) [5 bytes data]
587
+ buf: { (#3) [30 bytes data]
588
+ {
589
+ "answer": "now Bosnia and Herzegovina)\n"
590
+ }
591
+ buf: { (#3) [5 bytes data]
592
+ buf: { (#3) [20 bytes data]
593
+ {
594
+ "answer": "3. 1988 - Calgary,"
595
+ }
596
+ buf: { (#3) [5 bytes data]
597
+ buf: { (#3) [25 bytes data]
598
+ {
599
+ "answer": " Alberta, Canada\n4. 199"
600
+ }
601
+ buf: { (#3) [5 bytes data]
602
+ buf: { (#3) [26 bytes data]
603
+ {
604
+ "answer": "2 - Albertville, France\n"
605
+ }
606
+ buf: { (#3) [5 bytes data]
607
+ buf: { (#3) [13 bytes data]
608
+ {
609
+ "answer": "5. 1994 - L"
610
+ }
611
+ buf: { (#3) [5 bytes data]
612
+ buf: { (#3) [24 bytes data]
613
+ {
614
+ "answer": "illehammer, Norway\n6. "
615
+ }
616
+ buf: { (#3) [5 bytes data]
617
+ buf: { (#3) [23 bytes data]
618
+ {
619
+ "answer": "1998 - Nagano, Japan\n"
620
+ }
621
+ buf: { (#3) [5 bytes data]
622
+ buf: { (#3) [16 bytes data]
623
+ {
624
+ "answer": "7. 2002 - Salt"
625
+ }
626
+ buf: { (#3) [5 bytes data]
627
+ buf: { (#3) [24 bytes data]
628
+ {
629
+ "answer": " Lake City, Utah, USA\n"
630
+ }
631
+ buf: { (#3) [5 bytes data]
632
+ buf: { (#3) [18 bytes data]
633
+ {
634
+ "answer": "8. 2006 - Torino"
635
+ }
636
+ buf: { (#3) [5 bytes data]
637
+ buf: { (#3) [17 bytes data]
638
+ {
639
+ "answer": ", Italy\n9. 2010"
640
+ }
641
+ buf: { (#3) [5 bytes data]
642
+ buf: { (#3) [40 bytes data]
643
+ {
644
+ "answer": " - Vancouver, British Columbia, Canada"
645
+ }
646
+ buf: { (#3) [5 bytes data]
647
+ buf: { (#3) [13 bytes data]
648
+ {
649
+ "answer": "\n10. 2014 -"
650
+ }
651
+ buf: { (#3) [5 bytes data]
652
+ buf: { (#3) [20 bytes data]
653
+ {
654
+ "answer": " Sochi, Russia\n11."
655
+ }
656
+ buf: { (#3) [5 bytes data]
657
+ buf: { (#3) [16 bytes data]
658
+ {
659
+ "answer": " 2018 - Pyeong"
660
+ }
661
+ buf: { (#3) [5 bytes data]
662
+ buf: { (#3) [24 bytes data]
663
+ {
664
+ "answer": "chang, South Korea\n12."
665
+ }
666
+ buf: < (#3)
667
+ buf: < (#3) Grpc-Message:
668
+ buf: < (#3) Grpc-Status: 0
669
+ buf: * (#3) Call complete
670
+ buf: < (#2)
671
+ buf: < (#2) Grpc-Message:
672
+ buf: < (#2) Grpc-Status: 0
673
+ buf: * (#2) Call complete
674
+ %
675
+ ```
327
676
 
328
677
  ### 🔗 Multiple Services with Custom Interceptors
329
678
 
@@ -410,6 +759,74 @@ if __name__ == "__main__":
410
759
 
411
760
  TODO
412
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
+
413
830
  ### 🗄️ Protobuf file and code (Python files) generation using CLI
414
831
 
415
832
  You can genereate protobuf files and code for a given module and a specified class using `pydantic-rpc` CLI command:
@@ -439,16 +856,61 @@ Using this generated proto file and tools as `protoc`, `buf` and `BSR`, you coul
439
856
  | subclass of pydantic.BaseModel | message |
440
857
 
441
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
+
442
903
  ## TODO
443
- - [ ] Streaming Support
904
+ - [x] Streaming Support
444
905
  - [x] unary-stream
445
- - [ ] stream-unary
446
- - [ ] stream-stream
906
+ - [x] stream-unary
907
+ - [x] stream-stream
447
908
  - [ ] Betterproto Support
448
909
  - [ ] Sonora-connect Support
449
910
  - [ ] Custom Health Check Support
911
+ - [x] MCP (Model Context Protocol) Support via official MCP SDK
450
912
  - [ ] Add more examples
451
- - [ ] Add tests
913
+ - [x] Add tests
452
914
 
453
915
  ## 📜 License
454
916