A Tower-based Rust implementation of ConnectRPC, serving Connect, gRPC, and gRPC-Web clients over HTTP with binary or JSON protobuf messages.
Status: pre-1.0. The API surface is settling but may shift in 0.x. Production-quality runtime: passes the full ConnectRPC conformance suite — 3,600 server and 6,872 client tests across the three protocols.
MSRV: Rust 1.88 (declared on the workspace, verified in CI).
Documentation:
examples/ - runnable end-to-end examples (streaming, tower middleware, TLS, multi-service, browser/wasm, Bazel).connectrpc provides:
connectrpc — A Tower-based runtime library implementing the Connect protocolprotoc-gen-connect-rust — A protoc plugin that generates service traits, clients, and message typesconnectrpc-build — build.rs integration for generating code at build timeconnectrpc-health — The standard grpc.health.v1.Health service, for grpc_health_probe / kubelet gRPC probes / service-mesh health checksconnectrpc-reflection — The standard gRPC server reflection service (grpc.reflection.v1 + v1alpha), so grpcurl, buf curl, Postman, and grpcui can discover and call your servicesThe runtime is built on tower::Service, making it framework-agnostic. It integrates with any tower-compatible HTTP framework including Axum, Hyper, and others.
// greet.proto
syntax = "proto3";
package greet.v1;
service GreetService {
rpc Greet(GreetRequest) returns (GreetResponse);
}
message GreetRequest {
string name = 1;
}
message GreetResponse {
string greeting = 1;
}
Two workflows are supported. Both produce the same runtime API; pick the one that fits your build pipeline.
buf generate (recommended for checked-in code)Runs two codegen plugins (protoc-gen-buffa for message types,
protoc-gen-connect-rust for service stubs) and protoc-gen-buffa-packaging
twice to assemble the mod.rs module tree for each output directory. The
codegen plugins are invoked per-file; only the packaging plugin needs
strategy: all.
protoc-gen-buffa and protoc-gen-buffa-packaging ship from the
buffa repo - see its release
page for binaries or cargo install.
For protoc-gen-connect-rust, three options:
1. Download a pre-built binary from the GitHub release. Releases
ship Linux (x86_64, aarch64), macOS (x86_64, aarch64), and Windows
(x86_64) binaries, each with a SHA-256 checksum, a Sigstore signature
(.sig + .pem), and a GitHub-native build provenance attestation.
VERSION=v0.8.0
PLATFORM=linux-x86_64 # or darwin-aarch64, etc.
BASE=https://github.com/connectrpc/connect-rust/releases/download/${VERSION}
BIN=protoc-gen-connect-rust-${VERSION}-${PLATFORM}
curl -fSL -o "${BIN}" "${BASE}/${BIN}"
curl -fSL -o "${BIN}.sig" "${BASE}/${BIN}.sig"
curl -fSL -o "${BIN}.pem" "${BASE}/${BIN}.pem"
curl -fSL -o checksums-sha256.txt "${BASE}/checksums-sha256.txt"
# Verify the checksum.
grep " ${BIN}\$" checksums-sha256.txt | sha256sum -c -
# Verify the GitHub-native attestation (no .sig/.pem download needed).
gh attestation verify "${BIN}" --repo connectrpc/connect-rust
# Or verify the cosign signature directly.
cosign verify-blob \
--certificate "${BIN}.pem" \
--signature "${BIN}.sig" \
--certificate-identity "https://github.com/connectrpc/connect-rust/.github/workflows/release.yml@refs/tags/${VERSION}" \
--certificate-oidc-issuer "https://token.actions.githubusercontent.com" \
"${BIN}"
install -m 0755 "${BIN}" /usr/local/bin/protoc-gen-connect-rust
2. Build from source via cargo. Pulls the latest published
connectrpc-codegen crate from crates.io and installs the binary into
$CARGO_HOME/bin:
cargo install --locked connectrpc-codegen
3. Buf Schema Registry remote plugin (planned). Once accepted upstream
the plugin will be runnable as remote: buf.build/connectrpc/connect-rust
in buf.gen.yaml, with no local install step.
# buf.gen.yaml
version: v2
plugins:
- local: protoc-gen-buffa
out: src/generated/buffa
opt: [views=true, json=true]
- local: protoc-gen-buffa-packaging
out: src/generated/buffa
strategy: all
- local: protoc-gen-connect-rust
out: src/generated/connect
opt: [buffa_module=crate::proto]
- local: protoc-gen-buffa-packaging
out: src/generated/connect
strategy: all
opt: [filter=services]
// src/lib.rs
#[path = "generated/buffa/mod.rs"]
pub mod proto;
#[path = "generated/connect/mod.rs"]
pub mod connect;
buffa_module=crate::proto tells the service-stub generator where you
mounted the buffa output. For a method input type greet.v1.GreetRequest
it emits crate::proto::greet::v1::GreetRequest - the crate::proto root
you named, then the proto package as nested modules, then the type. The
second packaging invocation uses filter=services so the connect tree's
mod.rs only include!s files that actually have service stubs in them.
Changing the mount point requires regenerating.
The underlying option is
extern_path=.=crate::proto- same format the Buf Schema Registry uses when generating Cargo SDKs.buffa_module=Xis shorthand for the.catch-all case. Any module anextern_pathpoints at must be buffa-generated code from buffa 0.8.0 or newer with views enabled (buffa-types 0.8+ for the well-known types): the service stubs rely on theHasMessageViewimpls and owned-view wrappers that buffa generates alongside each message, just as they rely on the JSON serialization impls.
build.rs (generated at build time)Unified output: message types and service stubs in one file per proto,
assembled via a single include!. No plugin binaries required at build time.
[build-dependencies]
connectrpc-build = "0.8"
// build.rs
fn main() {
connectrpc_build::Config::new()
.files(&["proto/greet.proto"])
.includes(&["proto/"])
.include_file("_connectrpc.rs")
.compile()
.unwrap();
}
// lib.rs
pub mod proto {
connectrpc::include_generated!();
}
use connectrpc::{RequestContext, Response, ServiceRequest, ServiceResult};
struct MyGreetService;
impl GreetService for MyGreetService {
async fn greet(
&self,
_ctx: RequestContext,
request: ServiceRequest<'_, GreetRequest>,
) -> ServiceResult<GreetResponse> {
// `request` derefs to the view — string fields are borrowed `&str`
// directly from the request buffer (zero-copy). The borrow lives for
// the duration of the call; use `request.to_owned_message()` for
// anything that must outlive it (e.g. `tokio::spawn`).
Response::ok(GreetResponse {
greeting: format!("Hello, {}!", request.name),
..Default::default()
})
}
}
use axum::{Router, routing::get};
use connectrpc::Router as ConnectRouter;
use std::sync::Arc;
let connect = ConnectRouter::new().add_service(Arc::new(MyGreetService));
// Plain HTTP liveness probe for `kubectl`'s httpGet style. For the
// standard gRPC Health protocol (grpc_health_probe, kubelet `grpc:`
// probes), mount `connectrpc_health::HealthService` on the Connect
// router instead — see docs/guide.md#health-checking.
let app = Router::new()
.route("/health", get(|| async { "OK" }))
.fallback_service(connect.into_axum_service());
let listener = tokio::net::TcpListener::bind("0.0.0.0:8080").await?;
axum::serve(listener, app).await?;
For simple cases, enable the server feature for a built-in hyper server:
use connectrpc::{Router, Server};
use std::sync::Arc;
let router = Router::new().add_service(Arc::new(MyGreetService));
Server::new(router).serve("127.0.0.1:8080".parse()?).await?;
Enable the client feature for HTTP client support with connection pooling:
use connectrpc::client::{HttpClient, ClientConfig};
let http = HttpClient::plaintext(); // cleartext http:// only; use with_tls() for https://
let config = ClientConfig::new("http://localhost:8080".parse()?);
let client = GreetServiceClient::new(http, config);
let response = client.greet(GreetRequest {
name: "World".into(),
}).await?;
Generated clients expose both a no-options convenience method and a
_with_options variant for per-call control (timeout, headers, max
message size, compression override):
use connectrpc::client::CallOptions;
use std::time::Duration;
// Per-call timeout
let response = client.greet_with_options(
GreetRequest { name: "World".into() },
CallOptions::default().with_timeout(Duration::from_secs(5)),
).await?;
For options you want on every call (e.g. auth headers, a default
timeout), set them on ClientConfig instead — the no-options method
picks them up automatically:
let config = ClientConfig::new("http://localhost:8080".parse()?)
.with_default_timeout(Duration::from_secs(30))
.with_default_header("authorization", "Bearer ...");
let client = GreetServiceClient::new(http, config);
// Uses the 30s timeout and auth header without repeating them:
let response = client.greet(request).await?;
Per-call CallOptions override config defaults (options win).
The Quick Start above shows the unary path. For everything else, see the user guide and the focused examples:
examples/streaming-tour/ for all four RPC types side-by-side.Spec, headers, deadline, and a lazily decoded message body, and can rewrite or short-circuit the call - the equivalent of connect-go's WithInterceptors.examples/middleware/ for a custom auth layer that stamps caller identity into request extensions.examples/eliza/README.md for cert generation and Server::with_tls / HttpClient::with_tls patterns.grpc.health.v1.Health, used by grpc_health_probe, kubelet grpc: probes, and service meshes) - see docs/guide.md#health-checking and the connectrpc-health crate.grpc.reflection.v1 + v1alpha, used by grpcurl, buf curl, Postman, and grpcui) - see the connectrpc-reflection crate, and run examples/multiservice/reflection-demo.sh for a buf curl walkthrough against a live server.| Feature | Default | Description |
|---|---|---|
json |
Yes | JSON codec for protobuf messages. Disable (with codegen no_json) for proto-only builds — see Proto-only builds |
gzip |
Yes | Gzip compression via flate2 |
zstd |
Yes | Zstandard compression via zstd |
streaming |
Yes | Streaming compression via async-compression |
client |
No | HTTP client transports (plaintext) |
client-tls |
No | TLS for client transports (HttpClient::with_tls, Http2Connection::connect_tls) |
server |
No | Standalone hyper-based server |
server-tls |
No | TLS for the built-in server (Server::with_tls) |
tls |
No | Convenience: enables both server-tls + client-tls |
axum |
No | Axum framework integration |
The core crate compiles for wasm32-unknown-unknown. Generated clients are generic over ClientTransport, so they work on wasm with a custom transport (e.g. web-sys::fetch). The client/server/tls features require platform networking and zstd requires native C compilation. See examples/wasm-client for a complete Fetch-based transport.
[dependencies]
connectrpc = { version = "0.8", default-features = false, features = ["gzip"] }
```tom
$ claude mcp add connect-rust \
-- python -m otcore.mcp_server <graph>