Skip to main content

tonin_core/
lib.rs

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