Skip to main content

tonin_sdk/
lib.rs

1//! tonin-sdk: Service builder, Config, Context, Error, and runtime for tonin microservices.
2//!
3//! # Usage
4//!
5//! ```toml
6//! [dependencies]
7//! tonin-sdk = "0.4"
8//! ```
9//!
10//! ```no_run
11//! use tonin_sdk::prelude::*;
12//!
13//! #[tokio::main]
14//! async fn main() -> tonin_sdk::Result<()> {
15//!     Service::new("greeter").run().await
16//! }
17//! ```
18//!
19//! # Example
20//!
21//! ```no_run
22//! use tonin_sdk::{Service, Result};
23//! use tonin_sdk::auth::default::JwtValidator;
24//!
25//! #[tokio::main]
26//! async fn main() -> Result<()> {
27//!     let verifier = JwtValidator::from_env()?;
28//!     Service::new("greeter")
29//!         .with_auth(verifier)
30//!         .enable_mcp()
31//!         .handler(my_grpc_service())
32//!         .run()
33//!         .await
34//! }
35//!
36//! # use tonic::body::Body;
37//! # #[derive(Clone)]
38//! # struct MyGrpc;
39//! # impl tonic::server::NamedService for MyGrpc {
40//! #     const NAME: &'static str = "my.Grpc";
41//! # }
42//! # impl tower::Service<http::Request<Body>> for MyGrpc {
43//! #     type Response = http::Response<Body>;
44//! #     type Error = std::convert::Infallible;
45//! #     type Future = std::pin::Pin<Box<dyn std::future::Future<
46//! #         Output = std::result::Result<Self::Response, Self::Error>> + Send>>;
47//! #     fn poll_ready(&mut self, _: &mut std::task::Context<'_>)
48//! #         -> std::task::Poll<std::result::Result<(), Self::Error>> { unimplemented!() }
49//! #     fn call(&mut self, _: http::Request<Body>) -> Self::Future { unimplemented!() }
50//! # }
51//! # fn my_grpc_service() -> MyGrpc { MyGrpc }
52//! ```
53//!
54//! # Modules
55//!
56//! - [`auth`] — token extraction, verification, [`auth::AuthCtx`]
57//! - [`mcp`] — in-process MCP sidecar (second port, shared lifecycle)
58//! - [`telemetry`] — zero-config OTLP tracing + W3C TraceContext
59//! - [`transport`] — tonic/gRPC wiring helpers
60//! - [`discovery`] — k8s DNS-based service resolution
61//! - [`traits`] — capability traits (see below)
62//! - [`state`] — pre-wired DB + cache connections from env at boot
63//! - [`instrumented`] — OTel-span decorators around capability impls
64//! - [`job`] — background job / queue consumer surface
65//! - [`error`] — [`Error`] + [`Result`]
66//!
67//! # Capability traits
68//!
69//! [`traits::Cache`], [`traits::Database`], [`traits::EventBus`], and
70//! [`traits::SecretStore`] are the interface-first surface every service
71//! codes against. Concrete impls live in their own crates and are
72//! selected at deploy time via `tonin.toml` `engine = "..."` — swapping
73//! a backend is a TOML + `Cargo.toml` change, never a handler rewrite.
74//!
75//! # Sample app
76//!
77//! The canonical end-to-end example is
78//! [`examples/greeter`](https://github.com/Rushit/tonin/tree/main/examples/greeter):
79//! one proto, one `main.rs`, one `tonin.toml`.
80//!
81//! # Sibling crates
82//!
83//! - [`tonin`](https://docs.rs/tonin) — umbrella re-export + prelude
84//! - [`tonin-client`](https://docs.rs/tonin-client) — peer-service client primitives
85//! - [`tonin-mcp-macros`](https://docs.rs/tonin-mcp-macros) — `#[mcp_expose]` proc-macro
86//! - [`tonin-build`](https://docs.rs/tonin-build) — `build.rs` helper around tonic-build
87
88use std::net::SocketAddr;
89
90use tower::layer::util::{Identity, Stack};
91
92pub mod auth;
93pub mod discovery;
94pub mod error;
95pub mod instrumented;
96pub mod job;
97pub mod mcp;
98pub mod state;
99pub mod telemetry;
100pub mod traits;
101pub mod transport;
102
103pub use state::State;
104
105pub use error::{Error, Result};
106
107/// The tonic Router type after we install the trace-extract + auth
108/// layers. Both run on every request: telemetry extracts trace context
109/// first, then auth verifies the token.
110type LayeredRouter = tonic::transport::server::Router<
111    Stack<auth::AuthLayer, Stack<crate::telemetry::propagate::ExtractLayer, Identity>>,
112>;
113
114/// Type-erased factory that boots an MCP listener with a custom handler.
115///
116/// Service authors get one of these when they call
117/// [`Service::enable_mcp_with`]; the framework calls the boxed closure
118/// during `run()`. The handler type itself (e.g. `GreeterImplMcpAdapter`
119/// emitted by `#[mcp_expose]`) is erased here so `Service` can stay
120/// generic-free at the type level.
121pub(crate) type McpSpawner = Box<
122    dyn FnOnce(
123            crate::mcp::McpConfig,
124        ) -> std::pin::Pin<
125            Box<
126                dyn std::future::Future<
127                        Output = std::result::Result<
128                            (SocketAddr, tokio::task::JoinHandle<()>),
129                            std::io::Error,
130                        >,
131                    > + Send,
132            >,
133        > + Send,
134>;
135
136/// Service builder. Holds name, bind address, the tonic router being
137/// assembled, the optional auth layer, and the optional in-process MCP
138/// listener config + custom spawner.
139pub struct Service {
140    name: String,
141    addr: SocketAddr,
142    router: Option<LayeredRouter>,
143    auth_layer: Option<auth::AuthLayer>,
144    mcp: Option<crate::mcp::McpConfig>,
145    /// If set, called to spawn the MCP listener with a custom handler
146    /// (e.g. `GreeterImplMcpAdapter` from `#[mcp_expose]`). If None,
147    /// the default `McpServer` is used.
148    mcp_spawner: Option<McpSpawner>,
149}
150
151impl Service {
152    /// Construct a new service. Initializes OTLP tracing as a side effect
153    /// (idempotent; disabled if `TONIN_TELEMETRY=off`).
154    pub fn new(name: impl Into<String>) -> Self {
155        let name = name.into();
156        // Zero-config telemetry. Errors are logged but do not block startup —
157        // a service should still come up even if the collector is unreachable.
158        if let Err(e) = crate::telemetry::init(&name) {
159            eprintln!("micro: telemetry init failed: {e}");
160        }
161        Self {
162            name,
163            addr: "0.0.0.0:50051".parse().unwrap(),
164            router: None,
165            auth_layer: None,
166            mcp: None,
167            mcp_spawner: None,
168        }
169    }
170
171    pub fn addr(mut self, addr: SocketAddr) -> Self {
172        self.addr = addr;
173        self
174    }
175
176    /// Enable the in-process MCP listener on the default port
177    /// (`0.0.0.0:50052`). Same process, second port — MCP tool calls
178    /// see the same `AuthCtx` task-local, share DB/cache/storage
179    /// connections, and live or die with the gRPC server. To override
180    /// the port use [`Service::mcp_addr`].
181    ///
182    /// What this currently does: spawns a hyper listener that answers
183    /// `GET /health` with 200. The real MCP wire protocol (rmcp +
184    /// `#[tool]` macros bridging each gRPC method) lands in a
185    /// follow-up. This call only commits the framework to the
186    /// "in-process, second port" lifecycle.
187    pub fn enable_mcp(mut self) -> Self {
188        self.mcp = Some(crate::mcp::McpConfig::default());
189        self
190    }
191
192    /// Enable the in-process MCP listener on a specific address.
193    /// Useful for tests (bind `:0` to grab a random free port) or
194    /// for non-default deployments.
195    pub fn mcp_addr(mut self, addr: SocketAddr) -> Self {
196        self.mcp = Some(crate::mcp::McpConfig { addr });
197        self
198    }
199
200    /// Enable the in-process MCP listener with a **custom handler**.
201    /// Use this to wire a `#[tonin::mcp_expose]`-generated adapter
202    /// into the framework so MCP `tools/list` exposes one tool per
203    /// gRPC method on the user's impl.
204    ///
205    /// `factory` is invoked once per MCP session by rmcp's
206    /// `StreamableHttpService` — returning a fresh handler instance
207    /// each time (cheap if the handler is `Arc`-backed, which the
208    /// macro-generated adapter is).
209    ///
210    /// Example:
211    /// ```ignore
212    /// let svc = Service::new("greeter")
213    ///     .with_auth(auth::verifier())
214    ///     .enable_mcp_with(move || {
215    ///         Ok(GreeterImplMcpAdapter::new(GreeterImpl::new(state.clone())))
216    ///     });
217    /// ```
218    pub fn enable_mcp_with<H, F>(mut self, factory: F) -> Self
219    where
220        H: crate::mcp::McpServerHandler + Clone + Send + Sync + 'static,
221        F: Fn() -> std::result::Result<H, std::io::Error> + Send + Sync + Clone + 'static,
222    {
223        if self.mcp.is_none() {
224            self.mcp = Some(crate::mcp::McpConfig::default());
225        }
226        let spawner: McpSpawner = Box::new(move |cfg| {
227            Box::pin(async move { crate::mcp::spawn_with(cfg, factory).await })
228        });
229        self.mcp_spawner = Some(spawner);
230        self
231    }
232
233    /// Install an auth verifier with the default
234    /// [`auth::default::BearerHeaderExtractor`].
235    ///
236    /// Every inbound request runs the extractor → verifier chain. The
237    /// resulting [`auth::AuthCtx`] is placed in request extensions AND
238    /// in the [`auth::CURRENT_AUTH`] task-local for the handler's
239    /// execution.
240    ///
241    /// For custom token extraction (cookies, custom headers, etc.) build
242    /// an [`auth::AuthLayer`] directly and pass via
243    /// [`Service::with_auth_layer`].
244    pub fn with_auth<V: auth::TokenVerifier>(mut self, verifier: V) -> Self {
245        let extractor = auth::default::BearerHeaderExtractor;
246        self.auth_layer = Some(auth::AuthLayer::new(extractor, verifier));
247        self
248    }
249
250    /// Install a fully-customized auth layer. Use this when you need a
251    /// non-default token extractor (e.g. cookie-based auth).
252    pub fn with_auth_layer(mut self, layer: auth::AuthLayer) -> Self {
253        self.auth_layer = Some(layer);
254        self
255    }
256
257    /// Run all requests as anonymous. Handlers can still read
258    /// `AuthCtx::from(&req)` — they'll get
259    /// `AuthCtx { kind: PrincipalKind::Anonymous, .. }`.
260    ///
261    /// For services that genuinely don't authenticate (read-only public
262    /// APIs, internal-mesh-only with mTLS as the only check).
263    pub fn without_auth(mut self) -> Self {
264        let extractor = auth::default::BearerHeaderExtractor;
265        let verifier = auth::AnonymousVerifier;
266        self.auth_layer = Some(auth::AuthLayer::new(extractor, verifier).optional());
267        self
268    }
269
270    /// Attach a tonic-generated service. Codegen will call this for the user.
271    ///
272    /// Server-side trace-context extraction is installed once, the first
273    /// time a handler is added; every subsequent handler shares the layer.
274    ///
275    /// The auth layer (if configured via [`Service::with_auth`]) wraps
276    /// each individual service via tonic's per-service layering at
277    /// `run()` time, so it applies uniformly to every handler.
278    pub fn handler<S>(mut self, svc: S) -> Self
279    where
280        S: tower::Service<
281                http::Request<tonic::body::Body>,
282                Response = http::Response<tonic::body::Body>,
283                Error = std::convert::Infallible,
284            > + tonic::server::NamedService
285            + Clone
286            + Send
287            + Sync
288            + 'static,
289        S::Future: Send + 'static,
290    {
291        let auth_layer = self
292            .auth_layer
293            .clone()
294            .unwrap_or_else(default_anonymous_auth);
295        let router: LayeredRouter = match self.router.take() {
296            Some(r) => r.add_service(svc),
297            None => tonic::transport::Server::builder()
298                .http2_keepalive_interval(Some(std::time::Duration::from_secs(60)))
299                .http2_keepalive_timeout(Some(std::time::Duration::from_secs(20)))
300                .layer(crate::telemetry::propagate::extract_layer())
301                .layer(auth_layer)
302                .add_service(svc),
303        };
304        self.router = Some(router);
305        self
306    }
307
308    pub async fn run(self) -> Result<()> {
309        let Service {
310            name,
311            addr,
312            router,
313            auth_layer: _,
314            mcp,
315            mcp_spawner,
316        } = self;
317        let router = router.ok_or_else(|| Error::Config("no handler registered".into()))?;
318
319        // Serve grpc.health.v1.Health so Kubernetes `grpc:` probes work out of
320        // the box. Mark the overall ("") service SERVING; the auth layer
321        // allowlists this path so probes pass without credentials.
322        let (health_reporter, health_service) = tonic_health::server::health_reporter();
323        health_reporter
324            .set_service_status("", tonic_health::ServingStatus::Serving)
325            .await;
326        let router = router.add_service(health_service);
327
328        tracing::info!(service = %name, %addr, "tonin service starting");
329
330        // If MCP is enabled, spawn its listener in parallel. The
331        // gRPC server is still the "main" path — if MCP fails to
332        // bind we log loudly but don't abort, because a service that
333        // serves gRPC without MCP is still useful. (Auth-critical
334        // misconfig would have failed earlier at boot.)
335        let _mcp_handle = if let Some(cfg) = mcp {
336            // Use the custom spawner (from `enable_mcp_with`) if one
337            // was supplied; otherwise default to the bare McpServer
338            // with just the `health` tool.
339            let spawn_result = if let Some(spawner) = mcp_spawner {
340                spawner(cfg).await
341            } else {
342                crate::mcp::spawn(cfg).await
343            };
344            match spawn_result {
345                Ok((bound, handle)) => {
346                    tracing::info!(service = %name, mcp_addr = %bound, "mcp listener running");
347                    Some(handle)
348                }
349                Err(e) => {
350                    tracing::error!(service = %name, error = %e, "mcp listener failed to bind — continuing without MCP");
351                    None
352                }
353            }
354        } else {
355            None
356        };
357
358        let serve_result = router.serve(addr).await;
359        // Flush spans before the process exits.
360        crate::telemetry::shutdown();
361        serve_result?;
362        Ok(())
363    }
364}
365
366/// Default auth layer used when [`Service::with_auth`] / `without_auth`
367/// were not called. Treats every request as anonymous; handlers still
368/// see an `AuthCtx { kind: Anonymous }` via `AuthCtx::from(&req)`.
369fn default_anonymous_auth() -> auth::AuthLayer {
370    auth::AuthLayer::new(
371        auth::default::BearerHeaderExtractor,
372        auth::AnonymousVerifier,
373    )
374    .optional()
375}
376
377pub mod prelude {
378    // NOTE: `Result` is intentionally NOT in this prelude. It would
379    // shadow `std::result::Result` in user code, which breaks
380    // macro-generated code (rmcp's tool macros, custom derives) that
381    // emits unqualified `Result<T, E>` two-arg forms. Spell it as
382    // `tonin_sdk::Result<T>` when you want the framework's alias.
383    pub use super::{Error, Service, State};
384    pub use crate::auth::{AuthCtx, AuthError, PrincipalKind};
385    pub use crate::traits::{
386        Cache, Database, EventBus, MessageId, SecretStore, StartPosition, SubscribeOptions,
387    };
388    pub use tonic::{Request, Response, Status};
389}
390
391/// Convenience re-export: `#[tonin_sdk::main]` as an alias for `#[tokio::main]`.
392pub use tokio::main;