# endpoint-libs
[](https://crates.io/crates/endpoint-libs)
[](https://docs.rs/endpoint-libs)
[](https://github.com/pathscale/endpoint-libs/actions/workflows/rust.yml)
[](LICENSE)
[](https://deps.rs/crate/endpoint-libs/1.9.1)
Common library used across Pathscale projects. Contains generic utilities and shared types that are not specific to any single service, including a WebSocket server implementation, endpoint schema types for use with [endpoint-gen](https://github.com/pathscale/endpoint-gen), structured logging setup, and more.
## Releasing
Releases are managed with [`cargo-release`](https://github.com/crate-ci/cargo-release) and [`git-cliff`](https://github.com/orhun/git-cliff). Both must be installed:
```sh
cargo install cargo-release git-cliff
```
To cut a release:
```sh
The script will:
1. Run `cargo release --execute <level>` — bumps the version in `Cargo.toml`, updates the deps.rs badge in this README, regenerates `CHANGELOG.md`, and commits everything as `chore(release): vX.Y.Z`.
2. Open your `$EDITOR` with the auto-generated tag notes (from `git-cliff`) for review.
3. Create an annotated tag using the edited notes as the tag body (shown as GitHub Release notes).
4. Push the commit and tag.
5. Prompt whether to publish to crates.io.
To preview what `cargo-release` would do without making changes:
```sh
cargo release patch # omit --execute for a dry run
```
## Version Compatibility
When using `endpoint-libs` alongside `endpoint-gen` or `honey_id-types` in the same project, **minor versions must match** between all of them. Patch versions are stable across these crates but should ideally be kept in sync as well.
For example, `endpoint-libs 1.3.x` must be paired with `endpoint-gen 1.3.x` and `honey_id-types 1.3.x`.
## Features
The crate is feature-gated. The default feature set is `types` only.
### `types` (default)
Endpoint schema types shared between services and `endpoint-gen`:
- `Type`, `Field`, `EnumVariant` — the type system used to describe endpoint request/response schemas
- `TypeRegistry` and `Type::to_json_schema` — conversion of endpoint schemas to JSON Schema (used for MCP tool definitions, see below)
- Blockchain primitive types: `BlockchainAddress`, `BlockchainTransactionHash`, `U256`, `H256`
### `ws`
Async WebSocket server built on `tokio-tungstenite` with TLS support via `rustls`. Includes:
- Connection management and session tracking
- Push/subscription infrastructure
- Request handler and auth subcontroller traits with typed public errors
- HTTP header parsing helpers
Auth endpoints registered through `EndpointAuthController::add_auth_endpoint` use the same
typed error model as regular `RequestHandler` implementations. A `SubAuthController` declares
its generated request type and endpoint-local public error type, then returns `AuthResponse`.
```rust
use std::sync::Arc;
use endpoint_libs::libs::error_code::ErrorCode;
use endpoint_libs::libs::handler::HandlerError;
use endpoint_libs::libs::toolbox::{ArcToolbox, CustomError, RequestContext};
use endpoint_libs::libs::ws::{AuthResponse, SubAuthController, WsConnection};
use futures::future::LocalBoxFuture;
use futures::FutureExt;
pub struct MethodSignup;
pub enum SignupError {
UsernameTaken,
}
impl From<SignupError> for CustomError {
fn from(err: SignupError) -> Self {
match err {
SignupError::UsernameTaken => {
CustomError::new(ErrorCode::CONFLICT)
.with_message("username taken")
.with_kind("UsernameTaken")
}
}
}
}
impl SubAuthController for MethodSignup {
type Request = SignupRequest;
type Error = SignupError;
fn auth(
self: Arc<Self>,
_toolbox: &ArcToolbox,
req: SignupRequest,
_ctx: RequestContext,
conn: Arc<WsConnection>,
) -> LocalBoxFuture<'static, AuthResponse<SignupRequest, SignupError>> {
async move {
let user = create_user(req).await.map_err(HandlerError::internal)?;
conn.set_user_id(user.id);
conn.set_roles(Arc::new(vec![user.role as u32]));
Ok(SignupResponse { user_id: user.id })
}
.boxed_local()
}
}
```
### MCP (Model Context Protocol) support
The WebSocket server can optionally expose every registered endpoint as an
**MCP tool** over JSON-RPC 2.0, alongside the legacy `{method, seq, params}`
protocol. MCP is **off by default** and fully additive: with it disabled the
server behaves exactly as before, and even with it enabled, legacy frames are
routed unchanged — both protocols work on the same connection.
Supported MCP methods: `initialize`, `ping`, `tools/list` (filtered by the
connection's roles), `tools/call`, and `notifications/*`. Tool metadata
(`inputSchema` / `outputSchema`) is generated from the endpoint schemas via
`Type::to_json_schema` (`model::json_schema`, available under the default
`types` feature).
```rust
use endpoint_libs::libs::ws::mcp::McpServerInfo;
use endpoint_libs::model::TypeRegistry;
let mut server = WebsocketServer::new(config);
server.set_auth_controller(MyAuthController);
server.add_handler(MethodEcho);
// ... all other add_handler() calls ...
// With endpoint-gen generated code, use the generated `type_registry()`.
// Endpoints that only use primitive types can pass an empty registry.
let registry: TypeRegistry = type_registry();
server.enable_mcp(
®istry,
McpServerInfo { name: "my-service".into(), version: env!("CARGO_PKG_VERSION").into() },
)?;
server.listen().await
```
Behavior notes:
- **Frame detection** — a frame is treated as JSON-RPC iff it carries a
top-level `"jsonrpc": "2.0"` member, which legacy frames can never contain.
- **Tool names** — endpoint names in snake_case (`UserListSymbols` →
`user_list_symbols`).
- **Roles** — `tools/list` only shows tools the connection's roles allow;
calling a forbidden tool answers identically to an unknown tool.
- **Errors** — public handler errors (`CustomError`) become MCP tool results
with `isError: true`; invalid params map to `-32602`, unknown methods to
`-32601`, internal errors to `-32603` (with `logId` in `error.data`).
- **Streaming** — endpoints with a `stream_response` deliver only their
immediate response over MCP; stream frames are not forwarded (tools are
annotated accordingly in their description).
- `enable_mcp` fails at startup on unresolved `StructRef`/`EnumRef` names or
duplicate tool names, rather than serving broken schemas.
A runnable end-to-end example (MCP handshake + legacy frame on one
connection) is provided:
```sh
cargo run --example mcp_echo --features ws-http1
```
### `signal`
Unix signal handling (`SIGTERM`/`SIGINT`) with a global `CancellationToken` for coordinating graceful shutdown across async tasks.
### `scheduler`
Task scheduling utilities built on `tokio-cron-scheduler`:
- Fixed-interval repeated jobs
- `AdaptiveJob` — jobs whose interval can be changed at runtime via a `JobTrigger` handle
### `log_reader`
Utilities for reading and parsing structured log files, including reverse-line iteration for reading recent entries efficiently.
### `error_aggregation`
A `tracing` layer that captures recent error-level log events into an in-memory container, allowing them to be queried programmatically (e.g. to expose recent errors via an API endpoint).
### `log_throttling`
> **Do not use.** This feature is currently non-functional and is excluded from CI. It is present for future development only.
Rate-limiting layer for `tracing` events to suppress repeated log spam.
## Logging Setup
The `setup_logging` function (available without any optional features) provides a batteries-included `tracing` subscriber with:
- Stdout logging with thread names and line numbers
- Optional file logging with configurable rotation
- Runtime log level reloading via `LogReloadHandle`
- Optional `error_aggregation` layer (requires `error_aggregation` feature)
## OpenTelemetry (OTel) Integration
The logging framework supports forwarding all `tracing` spans and log events to an OpenTelemetry (OTLP) collector as primary signals (**Traces** and **Logs**). This operates as a parallel layer and does not affect stdout or file logging.
### Enabling OTel
To enable OTLP forwarding, configure `OtelConfig` in your `LoggingConfig`:
```rust
use std::collections::HashMap;
use endpoint_libs::libs::log::{LoggingConfig, OtelConfig, OtelProtocol};
let mut headers = HashMap::new();
headers.insert("x-api-key".to_string(), "your-token".to_string());
let config = LoggingConfig {
otel_config: OtelConfig {
enabled: true,
service_name: Some("my-service".into()),
endpoint: Some("http://localhost:4317".into()), // OTLP collector endpoint
protocol: OtelProtocol::Grpc,
headers,
},
..Default::default()
};
let setup = setup_logging(config)?;
// CRITICAL: Keep `setup.otel_guards` alive for the duration of the program.
// It flushes pending traces and logs to the collector on drop.
```
### Environment Variables
OTel can also be configured via standard environment variables:
| `OTEL_SERVICE_NAME` | Name of the service |
| `OTEL_EXPORTER_OTLP_TRACES_ENDPOINT` | Collector endpoint for traces |
| `OTEL_EXPORTER_OTLP_LOGS_ENDPOINT` | Collector endpoint for logs (falls back to traces endpoint) |
| `OTEL_EXPORTER_OTLP_PROTOCOL` | `grpc` or `http/protobuf` |
| `OTEL_EXPORTER_OTLP_HEADERS` | Key-value pairs for auth (e.g. `api-key=val,other=val`) |
| `OTEL_PROPAGATORS` | Context propagators (default: `tracecontext,baggage`) |
Note: Values in `OtelConfig` override environment variables. To prevent recursive logging, OTel internal crates are capped at the `WARN` level when global logging is set to `DEBUG` or `TRACE`.
## Config Loading
A `load_config` utility parses a JSON config file, defaulting to `etc/config.json` or overridden via `--config`/`CONFIG` env var. Supports an optional `--config-entry` for selecting a sub-key within the config object.