Skip to content
Merged
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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -41,3 +41,4 @@ Thumbs.db

# git
.worktrees
todo/
17 changes: 14 additions & 3 deletions mcp-servers/python/qr_code_server/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,9 @@ license = { text = "Apache-2.0" }
readme = "README.md"
requires-python = ">=3.11"
dependencies = [
"cryptography>=46.0.7", # Transitive pin
"cryptography>=48.0.1", # Transitive pin
"fastmcp>=3.2.4",
"mcp>=1.27.0",
"mcp>=1.28.1",
"numpy>=2.4.4",
"opencv-python-headless>=4.13.0.92",
"pydantic>=2.13.0",
Expand All @@ -20,7 +20,18 @@ dependencies = [
]

[tool.uv]
exclude-newer-package = { "cryptography" = "2026-04-11T23:59:59Z" }
exclude-newer-package = { "cryptography" = "2026-06-10T00:00:00Z" }
# Minimum versions for transitive dependencies.
constraint-dependencies = [
"authlib>=1.6.12",
"idna>=3.15",
"pillow>=12.3.0",
"pydantic-settings>=2.14.2",
"pygments>=2.20.0",
"pyjwt>=2.13.0",
"python-multipart>=0.0.31",
"starlette>=1.3.1",
]

[project.optional-dependencies]
dev = [
Expand Down
402 changes: 217 additions & 185 deletions mcp-servers/python/qr_code_server/uv.lock

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions mcp-servers/rust/filesystem-server/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions mcp-servers/rust/filesystem-server/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,9 @@ tracing = "0.1.44"
tracing-subscriber = { version = "0.3.22", features = ["env-filter"] }
uuid = { version = "1.23.0", features = ["v4"] }

[dev-dependencies]
tower = { version = "0.5", features = ["util"] }

[lints.clippy]
multiple_crate_versions = "allow"

Expand Down
24 changes: 20 additions & 4 deletions mcp-servers/rust/filesystem-server/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,28 +64,42 @@ make install # Install to ~/.cargo/bin
### Using Cargo Directly

```bash
# Run with roots
# Run with roots (binds to 127.0.0.1:8084 by default)
cargo run -- --roots /tmp /var/www /home/user/projects

# Expose on the network (requires a bearer token)
cargo run -- --roots /tmp --bind 0.0.0.0:8084 --auth-token "$FILESYSTEM_SERVER_AUTH_TOKEN"

# Test
cargo test
```

### CLI Options

| Flag | Default | Purpose |
|------|---------|---------|
| `--roots <paths...>` | (none) | Sandbox root directories (space-separated). |
| `--bind <ip:port>` | `127.0.0.1:8084` | Address the HTTP server binds to. Non-loopback addresses require `--auth-token`. |
| `--auth-token <token>` | (none) | Bearer token required in the `Authorization` header of every request. May also be set via the `FILESYSTEM_SERVER_AUTH_TOKEN` environment variable. |

### Using docker

```bash
# Single root
# Single root - containers must bind 0.0.0.0 to be reachable from the host,
# which requires a bearer token
docker run \
-p 8084:8084 \
-e FILESYSTEM_SERVER_AUTH_TOKEN="$FILESYSTEM_SERVER_AUTH_TOKEN" \
-v /tmp:/tmp \
filesystem-server --roots /tmp
filesystem-server --roots /tmp --bind 0.0.0.0:8084

# Multiple roots - mount and pass as arguments
docker run \
-p 8084:8084 \
-e FILESYSTEM_SERVER_AUTH_TOKEN="$FILESYSTEM_SERVER_AUTH_TOKEN" \
-v /var/www:/www \
-v /tmp:/tmp \
filesystem-server --roots "/www /tmp"
filesystem-server --roots "/www /tmp" --bind 0.0.0.0:8084
```
Image size: ~10 MB (binary: 3.2 MB + Debian slim base)

Expand Down Expand Up @@ -164,6 +178,8 @@ Makefile # Build & test automation

- **Sandbox**: All paths resolved against configured roots only
- **Symlink blocking**: Traversal across symlinks blocked
- **Loopback by default**: Binds to `127.0.0.1:8084`; binding to a non-loopback address is refused unless `--auth-token` is set
- **Bearer authentication**: When `--auth-token` (or `FILESYSTEM_SERVER_AUTH_TOKEN`) is set, every HTTP request must present `Authorization: Bearer <token>`; tokens are compared in constant time
- **Atomic writes**: File modifications are atomic via temporary files
- **Dry-run support**: `edit_file` with `dry_run=true` previews changes without modifying

Expand Down
216 changes: 209 additions & 7 deletions mcp-servers/rust/filesystem-server/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,15 +1,20 @@
use crate::sandbox::Sandbox;
use crate::server::{AppContext, FilesystemServer};
use anyhow::{Context, Result};
use axum::extract::State;
use axum::http::{Request, StatusCode, header};
use axum::middleware::Next;
use axum::response::{IntoResponse, Response};
use rmcp::transport;
use std::net::SocketAddr;
use std::sync::Arc;
use tracing_subscriber::EnvFilter;

pub mod sandbox;
pub mod server;
pub mod tools;

pub static DEFAULT_BIND_ADDRESS: &str = "0.0.0.0:8084";
pub static DEFAULT_BIND_ADDRESS: &str = "127.0.0.1:8084";
pub static APP_NAME: &str = env!("CARGO_PKG_NAME");
pub static APP_VERSION: &str = env!("CARGO_PKG_VERSION");
pub static MAX_FILE_SIZE: u64 = 1024 * 1024;
Expand All @@ -21,9 +26,74 @@ pub fn init_tracing() {
.try_init();
}

pub async fn build_router(roots: Vec<String>) -> Result<axum::Router> {
/// Bearer token required on all HTTP requests when authentication is enabled.
#[derive(Clone, Debug)]
pub struct AuthToken(String);

impl AuthToken {
pub fn new(token: &str) -> Self {
Self(token.to_string())
}

pub fn is_valid(&self, provided: &str) -> bool {
let expected = self.0.as_bytes();
let provided = provided.as_bytes();
if expected.is_empty() || expected.len() != provided.len() {
return false;
}
// Constant-time comparison so the token cannot be recovered one byte
// at a time through response-time measurement.
expected
.iter()
.zip(provided.iter())
.fold(0u8, |acc, (a, b)| acc | (a ^ b))
== 0
}
}

/// Parse and validate the bind address. Binding to a non-loopback address
/// without an auth token would expose the MCP endpoint to the network, so
/// refuse to start.
pub fn resolve_bind_address(bind: &str, auth_enabled: bool) -> Result<SocketAddr> {
let addr: SocketAddr = bind
.parse()
.with_context(|| format!("Invalid bind address '{}', expected IP:PORT", bind))?;
if !addr.ip().is_loopback() && !auth_enabled {
anyhow::bail!(
"Refusing to bind to non-loopback address '{}' without an auth token. \
Pass --auth-token (or set FILESYSTEM_SERVER_AUTH_TOKEN), or bind to a loopback address.",
bind
);
}
Ok(addr)
}

async fn require_bearer_token(
State(token): State<Arc<AuthToken>>,
request: Request<axum::body::Body>,
next: Next,
) -> Response {
let authorized = request
.headers()
.get(header::AUTHORIZATION)
.and_then(|value| value.to_str().ok())
.and_then(|value| value.strip_prefix("Bearer "))
.is_some_and(|provided| token.is_valid(provided));

if authorized {
next.run(request).await
} else {
(
StatusCode::UNAUTHORIZED,
[(header::WWW_AUTHENTICATE, "Bearer")],
"Unauthorized",
)
.into_response()
}
}

pub async fn build_router(roots: Vec<String>, auth_token: Option<String>) -> Result<axum::Router> {
let sandbox = Arc::new(Sandbox::new(roots).await.context("Could not add roots")?);
let processed_roots = &sandbox.get_roots();
let ctx = Arc::new(AppContext { sandbox });

let service = transport::streamable_http_server::StreamableHttpService::new(
Expand All @@ -34,22 +104,154 @@ pub async fn build_router(roots: Vec<String>) -> Result<axum::Router> {
transport::streamable_http_server::session::local::LocalSessionManager::default().into(),
Default::default(),
);
print_startup_banner(processed_roots);
Ok(axum::Router::new().nest_service("/mcp", service))

let mut router = axum::Router::new().nest_service("/mcp", service);
if let Some(token) = auth_token {
router = router.layer(axum::middleware::from_fn_with_state(
Arc::new(AuthToken::new(&token)),
require_bearer_token,
));
}
Ok(router)
}

pub fn print_startup_banner(roots: &Vec<String>) {
pub fn print_startup_banner(roots: &[String], bind: &SocketAddr, auth_enabled: bool) {
tracing::info!(
"----------- MCP SERVER -----------
App : {}
Version : {}
Roots : {:?}
Transport : Streamable-HTTP
Listening : http://{}/mcp
Auth : {}
",
APP_NAME,
APP_VERSION,
roots,
DEFAULT_BIND_ADDRESS,
bind,
if auth_enabled {
"bearer token required"
} else {
"disabled (loopback only)"
},
);
}

#[cfg(test)]
mod tests {
use super::*;
use axum::body::Body;
use axum::http::{Request, StatusCode, header};
use tempfile::TempDir;
use tower::ServiceExt;

#[test]
fn test_resolve_bind_address_loopback_without_token() {
let addr = resolve_bind_address("127.0.0.1:8084", false).unwrap();
assert!(addr.ip().is_loopback());
assert_eq!(addr.port(), 8084);
}

#[test]
fn test_resolve_bind_address_ipv6_loopback_without_token() {
let addr = resolve_bind_address("[::1]:8084", false).unwrap();
assert!(addr.ip().is_loopback());
}

#[test]
fn test_resolve_bind_address_wildcard_requires_token() {
// The old default of 0.0.0.0 with no authentication must refuse to
// start.
let result = resolve_bind_address("0.0.0.0:8084", false);
assert!(result.is_err());
assert!(result.unwrap_err().to_string().contains("non-loopback"));
}

#[test]
fn test_resolve_bind_address_wildcard_with_token() {
let addr = resolve_bind_address("0.0.0.0:8084", true).unwrap();
assert_eq!(addr.port(), 8084);
}

#[test]
fn test_resolve_bind_address_external_ip_requires_token() {
assert!(resolve_bind_address("192.168.1.10:9000", false).is_err());
assert!(resolve_bind_address("192.168.1.10:9000", true).is_ok());
}

#[test]
fn test_resolve_bind_address_invalid() {
assert!(resolve_bind_address("not-an-address", true).is_err());
}

#[test]
fn test_auth_token_comparison() {
let token = AuthToken::new("s3cret");
assert!(token.is_valid("s3cret"));
assert!(!token.is_valid("wrong"));
assert!(!token.is_valid("s3cret-extra"));
assert!(!token.is_valid(""));
}

#[test]
fn test_auth_token_empty_never_valid() {
// Security: an empty configured token must not authenticate the
// empty bearer string.
let token = AuthToken::new("");
assert!(!token.is_valid(""));
}

async fn router_fixture(auth_token: Option<&str>) -> axum::Router {
let temp_dir = TempDir::new().unwrap();
let root = temp_dir.path().to_string_lossy().to_string();
let router = build_router(vec![root], auth_token.map(str::to_string))
.await
.unwrap();
// Leak the TempDir so the sandbox root outlives the fixture; the OS
// reclaims it on process exit.
std::mem::forget(temp_dir);
router
}

fn mcp_request(auth_header: Option<&str>) -> Request<Body> {
let mut builder = Request::builder().uri("/mcp");
if let Some(value) = auth_header {
builder = builder.header(header::AUTHORIZATION, value);
}
builder.body(Body::empty()).unwrap()
}

#[tokio::test]
async fn test_router_without_token_allows_unauthenticated() {
let router = router_fixture(None).await;
let response = router.oneshot(mcp_request(None)).await.unwrap();
assert_ne!(response.status(), StatusCode::UNAUTHORIZED);
}

#[tokio::test]
async fn test_router_with_token_rejects_missing_header() {
let router = router_fixture(Some("s3cret")).await;
let response = router.oneshot(mcp_request(None)).await.unwrap();
assert_eq!(response.status(), StatusCode::UNAUTHORIZED);
}

#[tokio::test]
async fn test_router_with_token_rejects_wrong_token() {
let router = router_fixture(Some("s3cret")).await;
let response = router
.oneshot(mcp_request(Some("Bearer wrong")))
.await
.unwrap();
assert_eq!(response.status(), StatusCode::UNAUTHORIZED);
}

#[tokio::test]
async fn test_router_with_token_accepts_correct_token() {
let router = router_fixture(Some("s3cret")).await;
let response = router
.oneshot(mcp_request(Some("Bearer s3cret")))
.await
.unwrap();
assert_ne!(response.status(), StatusCode::UNAUTHORIZED);
}
}
Loading
Loading