Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,26 @@ All notable changes to this project will be documented in this file.
Historical entries use distinct `example*` pseudonyms where retired service
identifiers were removed. Commit links remain the authoritative historical record.

## Unreleased

### Added

- **proxy:** `labby proxy <stdio-server>` now exposes one stdio MCP server
faithfully over loopback or an owned Tailscale Serve HTTPS port. Zero-flag
defaults support tailnet policy, while setup can persist separate bearer or
stable-issuer OAuth policy, random or fixed ports, endpoint path, child
environment inheritance, and cleanup preferences.
- **setup/doctor:** `labby setup proxy` stores non-secrets in `config.toml`,
generates or accepts bearer secrets in the hardened `.env`, and remains
idempotent. Zero-route `labby doctor proxy` validates local proxy
prerequisites without removing the existing routed reverse-proxy doctor.
- **oauth:** ephemeral exact-resource leases register each OAuth proxy URL,
including port and path, through admin gateway actions; the proxy renews and
releases leases while the daemon expires crash leftovers by TTL.
- **docs:** added the stdio MCP proxy operator guide plus generated proxy
configuration, environment, service, action, CLI, API, and OpenAPI inventory
coverage with drift tests.

## [1.8.5](https://github.com/dinglebear-ai/labby/compare/v1.8.4...v1.8.5) (2026-07-30)


Expand Down
4 changes: 2 additions & 2 deletions Justfile
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,11 @@ test-cargo-wrapper:

# Regenerate code-owned documentation inventories
docs-generate:
cargo run --package labby --all-features -- docs generate
cargo run --package labby --bin labby --all-features -- docs generate

# Verify generated documentation inventories are fresh
docs-check:
cargo run --package labby --all-features -- docs check
cargo run --package labby --bin labby --all-features -- docs check

# Run integration tests (requires running services)
test-integration:
Expand Down
36 changes: 34 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,10 @@ Labby is centered on the current gateway/operator surface:
tools/resources/prompts, apply exposure filters, publish protected MCP routes,
and optionally collapse the upstream catalog into Code Mode `search` and
`execute`.
- **Direct stdio proxy** - launch one stdio MCP server with
`labby proxy /path/to/dist.js` and expose its unmodified MCP surface over
loopback or an owned Tailscale Serve HTTPS port with tailnet, bearer, OAuth,
or explicit no-auth policy.
- **Authentication and protected routes** - run bearer or OAuth authentication,
manage route-scoped access, authorize upstream OAuth connections, and publish
protected MCP endpoints.
Expand All @@ -41,7 +45,8 @@ Labby is centered on the current gateway/operator surface:
- **Incus and bare-metal setup** - provision and operate a dedicated Labby
gateway host without introducing a separate fleet or deployment product.
- **Generated discovery** - publish code-owned service, action, environment,
API route, OpenAPI, MCP help, CLI help, and feature-matrix artifacts under
proxy configuration, API route, OpenAPI, MCP help, CLI help, and
feature-matrix artifacts under
[docs/generated](./docs/generated/README.md).

The registered services on this branch are exactly `doctor`, `fs`, `gateway`,
Expand All @@ -54,6 +59,31 @@ instead of copying command or action lists by hand.

## Quick Start

### Proxy One Stdio MCP Server

After installing Labby, configure proxy defaults once and launch a JavaScript
stdio server without proxy flags:

```bash
labby setup proxy
labby doctor proxy
labby proxy /path/to/dist.js
```

The built-in zero-flag policy is Tailscale Serve plus tailnet authorization on
a random high port. Child flags follow the first child token unchanged, and an
explicit separator is available for unusual commands:

```bash
labby proxy /path/to/dist.js --workspace /srv/data --read-only
labby proxy -- npx -y @modelcontextprotocol/server-filesystem /srv/data
```

Use `labby proxy --local --auth none ...` for explicit loopback-only
development. Bearer and OAuth setup, exact-port resource audiences, safe Serve
ownership, configuration precedence, output modes, and recovery are covered in
the [stdio MCP proxy guide](./docs/guides/STDIO_MCP_PROXY.md).

### Install A Release

Linux/macOS:
Expand Down Expand Up @@ -244,7 +274,9 @@ without the `gateway` feature.
labby doctor # audit every configured service
labby doctor system # local env vars, Docker, disk, toolchain
labby doctor auth # auth/OAuth env vars, files, permissions
labby doctor proxy # public Lab and protected MCP proxy endpoints
labby doctor proxy # zero-route stdio-proxy config/dependency preflight
labby doctor proxy --app-url URL --mcp-url URL --route /path
# routed public reverse-proxy checks remain available
labby doctor oauth-relay
labby health # lightweight liveness/readiness probe
labby logs # tail the active deployment's service journal
Expand Down
26 changes: 24 additions & 2 deletions config/config.example.toml
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,9 @@
# Docker and local `labby serve` share the same config.
#
# Secrets (API keys, tokens, passwords, URLs) belong in ~/.labby/.env.
# Everything else belongs here. All values below can still be overridden
# by environment variables — env always wins over config.toml.
# Everything else belongs here. Only settings with a documented environment
# alias can be overridden that way; proxy preferences intentionally do not
# invent one-to-one aliases. Where an alias exists, env wins over config.toml.
#
# Config search order (first found wins):
# 1. ./config.toml (repo/CWD override)
Expand Down Expand Up @@ -58,6 +59,27 @@
# Override with LAB_MCP_ALLOWED_HOSTS env var (comma-separated).
# allowed_hosts = [] # default: []

# ── Direct stdio MCP proxy ──────────────────────────────────────────────────

[proxy]
# Publish through one owned Tailscale Serve HTTPS port, or keep loopback-only.
# exposure = "tailscale" # tailscale | local

# tailnet uses Tailscale reachability/grants without an application token.
# auth = "tailnet" # tailnet | bearer | oauth | none
# path = "/mcp" # absolute, non-root, no query/fragment
# port = "random" # random or fixed integer
# port_range_start = 49152
# port_range_end = 65535

# The secret value belongs in ~/.labby/.env, never here.
# bearer_token_env = "LABBY_PROXY_BEARER_TOKEN"
# oauth_scopes = ["mcp:read", "mcp:write"]

# Ambient child variables beyond the runtime-essential scrubbed allowlist.
# inherit_env = []
# shutdown_grace_ms = 3000 # valid range: 1..=60000

# ── HTTP API ─────────────────────────────────────────────────────────────────

[api]
Expand Down
5 changes: 5 additions & 0 deletions crates/labby-auth/src/jwt.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ pub struct AccessClaims {
pub sub: String,
pub aud: String,
pub exp: usize,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub nbf: Option<usize>,
pub iat: usize,
pub jti: String,
pub scope: String,
Expand Down Expand Up @@ -128,6 +130,7 @@ impl SigningKeys {
) -> Result<AccessClaims, AuthError> {
let mut validation = Validation::new(Algorithm::EdDSA);
validation.set_audience(&[expected_audience]);
validation.validate_nbf = true;
decode::<AccessClaims>(token, &self.decoding_key, &validation)
.map(|data| data.claims)
.map_err(|_| AuthError::InvalidAccessToken)
Expand All @@ -146,6 +149,7 @@ impl SigningKeys {
let mut validation = Validation::new(Algorithm::EdDSA);
validation.set_audience(&[expected_audience]);
validation.set_issuer(&[expected_issuer]);
validation.validate_nbf = true;
decode::<AccessClaims>(token, &self.decoding_key, &validation)
.map(|data| data.claims)
.map_err(|_| AuthError::InvalidAccessToken)
Expand Down Expand Up @@ -302,6 +306,7 @@ mod tests {
sub: "google-user".to_string(),
aud: "https://lab.example.com".to_string(),
exp: 4_102_444_800,
nbf: None,
iat: 1_700_000_000,
jti: "test-jti".to_string(),
scope: "lab".to_string(),
Expand Down
5 changes: 4 additions & 1 deletion crates/labby-auth/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ pub mod metadata;
pub mod middleware;
#[cfg(feature = "http-axum")]
mod remote;
pub mod resource_registry;
#[cfg(feature = "http-axum")]
pub mod routes;
#[cfg(feature = "http-axum")]
Expand All @@ -33,7 +34,9 @@ pub mod util;
#[cfg(feature = "http-axum")]
pub use auth_context::{AuthContext, auth_context, www_authenticate_value};
#[cfg(feature = "http-axum")]
pub use middleware::{ActorKeyDeriver, AuthLayer, AuthService, parse_bearer_token, tokens_equal};
pub use middleware::{
ActorKeyDeriver, AuthLayer, AuthService, RequiredScopes, parse_bearer_token, tokens_equal,
};

#[cfg(test)]
pub mod test_support;
Loading
Loading