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}