Install
$ agentstack add mcp-i2y-pydantic-rpc Open-source listing, not yet scanned by AgentStack. Follow the source repository for install instructions.
Security review
⚠ Flagged2 finding(s); flagged for manual review. · v0.1.0 How review works →
- • Prompt-injection patterns
- • Secret / credential exfiltration
- • Dangerous shell & filesystem operations
- • Untrusted network calls
- • Known-malicious package signatures
- high Dangerous shell/eval execution.
- high Pipes remote content directly into a shell (remote code execution).
What it can access
- ● Network access Used
- ✓ Filesystem access No
- ✓ Shell / process execution No
- ✓ Environment & secrets No
- ● Dynamic code execution Used
From automated source analysis of v0.1.0. “Used” means the capability is present in the source — more access means more to trust, not that it’s unsafe.
Reliability & compatibility
Declared compatibility
Compatibility is declared by the source manifest. End-to-end runtime verification is coming, see below.
We're building live execution health for every listing: tool-call success rate, median latency, uptime, and last-checked timestamps, measured, not self-reported. It isn't live yet, so we don't show numbers we can't stand behind.
How agent discovery & health will work →About
🚀 PydanticRPC
PydanticRPC is a Python library that enables you to rapidly expose Pydantic models via gRPC/Connect RPC 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.
Below is an example of a simple gRPC service that exposes a PydanticAI agent:
import asyncio
from openai import AsyncOpenAI
from pydantic_ai import Agent
from pydantic_ai.models.openai import OpenAIModel
from pydantic_rpc import AsyncIOServer, Message
# `Message` is just an alias for Pydantic's `BaseModel` class.
class CityLocation(Message):
city: str
country: str
class Olympics(Message):
year: int
def prompt(self):
return f"Where were the Olympics held in {self.year}?"
class OlympicsLocationAgent:
def __init__(self):
client = AsyncOpenAI(
base_url="http://localhost:11434/v1",
api_key="ollama_api_key",
)
ollama_model = OpenAIModel(
model_name="llama3.2",
openai_client=client,
)
self._agent = Agent(ollama_model)
async def ask(self, req: Olympics) -> CityLocation:
result = await self._agent.run(req.prompt())
return result.data
if __name__ == "__main__":
# New enhanced initialization API (optional - backward compatible)
s = AsyncIOServer(service=OlympicsLocationAgent(), port=50051)
loop = asyncio.get_event_loop()
loop.run_until_complete(s.run())
And here is an example of a simple Connect RPC service that exposes the same agent as an ASGI application:
import asyncio
from openai import AsyncOpenAI
from pydantic_ai import Agent
from pydantic_ai.models.openai import OpenAIModel
from pydantic_rpc import ASGIApp, Message
class CityLocation(Message):
city: str
country: str
class Olympics(Message):
year: int
def prompt(self):
return f"Where were the Olympics held in {self.year}?"
class OlympicsLocationAgent:
def __init__(self):
client = AsyncOpenAI(
base_url="http://localhost:11434/v1",
api_key="ollama_api_key",
)
ollama_model = OpenAIModel(
model_name="llama3.2",
openai_client=client,
)
self._agent = Agent(ollama_model, result_type=CityLocation)
async def ask(self, req: Olympics) -> CityLocation:
result = await self._agent.run(req.prompt())
return result.data
# New enhanced initialization API (optional - backward compatible)
app = ASGIApp(service=OlympicsLocationAgent())
💡 Key Features
- 🔄 Automatic Protobuf Generation: Automatically creates protobuf files matching the method signatures of your Python objects.
- ⚙️ Dynamic Code Generation: Generates server and client stubs using
grpcio-tools. - ✅ Pydantic Integration: Uses
pydanticfor robust type validation and serialization. - 📄 Pprotobuf File Export: Exports the generated protobuf files for use in other languages.
- For gRPC:
- 💚 Health Checking: Built-in support for gRPC health checks using
grpc_health.v1. - 🔎 Server Reflection: Built-in support for gRPC server reflection.
- ⚡ Asynchronous Support: Easily create asynchronous gRPC services with
AsyncIOServer. - For Connect-RPC:
- 🌐 Full Protocol Support: Native Connect-RPC support via
connect-python - 🔄 All Streaming Patterns: Unary, server streaming, client streaming, and bidirectional streaming
- 🌐 WSGI/ASGI Applications: Run as standard WSGI or ASGI applications for easy deployment
- 🛠️ Pre-generated Protobuf Files and Code: Pre-generate proto files and corresponding code via the CLI. By setting the environment variable (PYDANTICRPCSKIP_GENERATION), you can skip runtime generation.
- 🤖 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.
⚠️ Important Notes for Connect-RPC
When using Connect-RPC with ASGIApp:
- Endpoint Path Format: Connect-RPC endpoints use CamelCase method names in the path:
/./(e.g.,/chat.v1.ChatService/SendMessage) - Content-Type: Set
Content-Type: application/jsonorapplication/connect+jsonfor requests - HTTP/2 Requirement: Bidirectional streaming requires HTTP/2. Use Hypercorn instead of uvicorn for HTTP/2 support
- Testing: Use buf curl for testing Connect-RPC endpoints with proper streaming support
For detailed examples and testing instructions, see the [examples directory](examples/).
📦 Installation
Install PydanticRPC via pip:
pip install pydantic-rpc
For CLI support with built-in server runners:
pip install pydantic-rpc-cli # Includes hypercorn and gunicorn
🆕 Enhanced Features (v0.10.0+)
Note: All new features are fully backward compatible. Existing code continues to work without modification.
Enhanced Initialization API
All server classes now support optional initialization with services:
# Traditional API (still works)
server = AsyncIOServer()
server.set_port(50051)
await server.run(MyService())
# New enhanced API (optional)
server = AsyncIOServer(
service=MyService(),
port=50051,
package_name="my.package"
)
await server.run()
# Same for ASGI/WSGI apps
app = ASGIApp(service=MyService(), package_name="my.package")
Error Handling with Decorators
Automatically map exceptions to gRPC/Connect status codes:
from pydantic_rpc import error_handler
import grpc
class MyService:
@error_handler(ValidationError, status_code=grpc.StatusCode.INVALID_ARGUMENT)
@error_handler(KeyError, status_code=grpc.StatusCode.NOT_FOUND)
async def get_user(self, request: GetUserRequest) -> User:
# Exceptions are automatically converted to proper status codes
if request.id not in users_db:
raise KeyError(f"User {request.id} not found")
return users_db[request.id]
🚀 Getting Started
PydanticRPC supports two main protocols:
- gRPC: Traditional gRPC services with
ServerandAsyncIOServer - Connect-RPC: Modern HTTP-based RPC with
ASGIAppandWSGIApp
🔧 Synchronous gRPC Service Example
from pydantic_rpc import Server, Message
class HelloRequest(Message):
name: str
class HelloReply(Message):
message: str
class Greeter:
# Define methods that accepts a request and returns a response.
def say_hello(self, request: HelloRequest) -> HelloReply:
return HelloReply(message=f"Hello, {request.name}!")
if __name__ == "__main__":
server = Server()
server.run(Greeter())
⚙️ Asynchronous gRPC Service Example
import asyncio
from pydantic_rpc import AsyncIOServer, Message
class HelloRequest(Message):
name: str
class HelloReply(Message):
message: str
class Greeter:
async def say_hello(self, request: HelloRequest) -> HelloReply:
return HelloReply(message=f"Hello, {request.name}!")
async def main():
# You can specify a custom port (default is 50051)
server = AsyncIOServer(port=50052)
await server.run(Greeter())
if __name__ == "__main__":
asyncio.run(main())
The AsyncIOServer automatically handles graceful shutdown on SIGTERM and SIGINT signals.
🌐 Connect-RPC ASGI Application Example
from pydantic_rpc import ASGIApp, Message
class HelloRequest(Message):
name: str
class HelloReply(Message):
message: str
class Greeter:
async def say_hello(self, request: HelloRequest) -> HelloReply:
return HelloReply(message=f"Hello, {request.name}!")
app = ASGIApp()
app.mount(Greeter())
# Run with uvicorn:
# uvicorn script:app --host 0.0.0.0 --port 8000
🌐 Connect-RPC WSGI Application Example
from pydantic_rpc import WSGIApp, Message
class HelloRequest(Message):
name: str
class HelloReply(Message):
message: str
class Greeter:
def say_hello(self, request: HelloRequest) -> HelloReply:
return HelloReply(message=f"Hello, {request.name}!")
app = WSGIApp()
app.mount(Greeter())
# Run with gunicorn:
# gunicorn script:app
🏆 Connect-RPC with Streaming Example
PydanticRPC provides native Connect-RPC support via connect-python, including full streaming capabilities and PEP 8 naming conventions. Check out our ASGI examples:
# Run with uvicorn
uv run uvicorn greeting_asgi:app --port 3000
# Or run streaming example
uv run python examples/streaming_connect_python.py
This will launch a connect-python-based ASGI application that uses the same Pydantic models to serve Connect-RPC requests.
Streaming Support with connect-python
connect-python provides full support for streaming RPCs with automatic PEP 8 naming (snake_case):
from typing import AsyncIterator
from pydantic_rpc import ASGIApp, Message
class StreamRequest(Message):
text: str
count: int
class StreamResponse(Message):
text: str
index: int
class StreamingService:
# Server streaming
async def server_stream(self, request: StreamRequest) -> AsyncIterator[StreamResponse]:
for i in range(request.count):
yield StreamResponse(text=f"{request.text}_{i}", index=i)
# Client streaming
async def client_stream(self, requests: AsyncIterator[StreamRequest]) -> StreamResponse:
texts = []
async for req in requests:
texts.append(req.text)
return StreamResponse(text=" ".join(texts), index=len(texts))
# Bidirectional streaming
async def bidi_stream(
self, requests: AsyncIterator[StreamRequest]
) -> AsyncIterator[StreamResponse]:
idx = 0
async for req in requests:
yield StreamResponse(text=f"Echo: {req.text}", index=idx)
idx += 1
app = ASGIApp()
app.mount(StreamingService())
> [!NOTE] > Please install protoc-gen-connect-python to run the connect-python example.
♻️ Skipping Protobuf Generation
By default, PydanticRPC generates .proto files and code at runtime. If you wish to skip the code-generation step (for example, in production environment), set the environment variable below:
export PYDANTIC_RPC_SKIP_GENERATION=true
When this variable is set to "true", PydanticRPC will load existing pre-generated modules rather than generating theƒm on the fly.
🪧 Setting Protobuf and Connect RPC/gRPC generation directory
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:
export PYDANTIC_RPC_PROTO_PATH=/your/path
⚠️ Reserved Fields
You can also set an environment variable to reserve a set number of fields for proto generation, for backward and forward compatibility.
export PYDANTIC_RPC_RESERVED_FIELDS=1
💎 Advanced Features
🌊 Response Streaming (gRPC)
PydanticRPC supports streaming responses for both gRPC and Connect-RPC services. If a service class method's return type is typing.AsyncIterator[T], the method is considered a streaming method.
Please see the sample code below:
import asyncio
from typing import Annotated, AsyncIterator
from openai import AsyncOpenAI
from pydantic import Field
from pydantic_ai import Agent
from pydantic_ai.models.openai import OpenAIModel
from pydantic_rpc import AsyncIOServer, Message
# `Message` is just a pydantic BaseModel alias
class CityLocation(Message):
city: Annotated[str, Field(description="The city where the Olympics were held")]
country: Annotated[
str, Field(description="The country where the Olympics were held")
]
class OlympicsQuery(Message):
year: Annotated[int, Field(description="The year of the Olympics", ge=1896)]
def prompt(self):
return f"Where were the Olympics held in {self.year}?"
class OlympicsDurationQuery(Message):
start: Annotated[int, Field(description="The start year of the Olympics", ge=1896)]
end: Annotated[int, Field(description="The end year of the Olympics", ge=1896)]
def prompt(self):
return f"From {self.start} to {self.end}, how many Olympics were held? Please provide the list of countries and cities."
class StreamingResult(Message):
answer: Annotated[str, Field(description="The answer to the query")]
class OlympicsAgent:
def __init__(self):
client = AsyncOpenAI(
base_url='http://localhost:11434/v1',
api_key='ollama_api_key',
)
ollama_model = OpenAIModel(
model_name='llama3.2',
openai_client=client,
)
self._agent = Agent(ollama_model)
async def ask(self, req: OlympicsQuery) -> CityLocation:
result = await self._agent.run(req.prompt(), result_type=CityLocation)
return result.data
async def ask_stream(
self, req: OlympicsDurationQuery
) -> AsyncIterator[StreamingResult]:
async with self._agent.run_stream(req.prompt(), result_type=str) as result:
async for data in result.stream_text(delta=True):
yield StreamingResult(answer=data)
if __name__ == "__main__":
s = AsyncIOServer()
loop = asyncio.get_event_loop()
loop.run_until_complete(s.run(OlympicsAgent()))
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.
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.
% buf curl --data '{"start": 1980, "end": 2024}' -v http://localhost:50051/olympicsagent.v1.OlympicsAgent/AskStream --protocol grpc --http2-prior-knowledge
buf: * Using server reflection to resolve "olympicsagent.v1.OlympicsAgent"
buf: * Dialing (tcp) localhost:50051...
buf: * Connected to [::1]:50051
buf: > (#1) POST /grpc.reflection.v1.ServerReflection/ServerReflectionInfo
buf: > (#1) Accept-Encoding: identity
buf: > (#1) Content-Type: application/grpc+proto
buf: > (#1) Grpc-Accept-Encoding: gzip
buf: > (#1) Grpc-Timeout: 119997m
buf: > (#1) Te: trailers
buf: > (#1) User-Agent: grpc-go-connect/1.12.0 (go1.21.4) buf/1.28.1
buf: > (#1)
buf: } (#1) [5 bytes data]
buf: } (#1) [32 bytes data]
buf: (#2) POST /grpc.reflection.v1alpha.ServerReflection/ServerReflectionInfo
buf: > (#2) Accept-Encoding: identity
buf: > (#2) Content-Type: application/grpc+proto
buf: > (#2) Grpc-Accept-Encoding: gzip
buf: > (#2) Grpc-Timeout: 119967m
buf: > (#2) Te: trailers
buf: > (#2) User-Agent: grpc-go-connect/1.12.0 (go1.21.4) buf/1.28.1
buf: > (#2)
buf: } (#2) [5 bytes data]
buf: } (#2) [32 bytes data]
buf: (#3) POST /olympicsagent.v1.OlympicsAgent/AskStream
buf: > (#3) Accept-Encoding: identity
buf: > (#3) Content-Type: application/grpc+proto
buf: > (#3) Grpc-Accept-Encoding: gzip
buf: > (#3) Grpc-Timeout: 119947m
buf: > (#3) Te: trailers
buf: > (#3) User-Agent: grpc-go-connect/1.12.0 (go1.21.4) buf/1.28.1
buf: > (#3)
buf: } (#3) [5 bytes data]
buf: } (#3) [6 bytes data]
buf: * (#3) Finished upload
buf: GreetingResponse:
return GreetingResponse(message="Hello!")
async def get_default_greeting(self) -> GreetingResponse:
# Method with no request parameter (implicitly empty)
return GreetingResponse(message="Hello, World!")
🎨 Custom Serialization
Pydantic's serialization decorators are fully supported:
from typing import Any
from pydantic import field_serializer, mo
…
## Source & license
This open-source MCP server is cataloged on AgentStack and links to its original source — we do not rehost the code.
- **Author:** [i2y](https://github.com/i2y)
- **Source:** [i2y/pydantic-rpc](https://github.com/i2y/pydantic-rpc)
- **License:** MIT
Install and usage instructions live in the source repository linked above.
Reviews
No reviews yet, be the first.
Write a review
Versions
- v0.1.0 Imported from the upstream source.