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
129 changes: 129 additions & 0 deletions Cargo.lock

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

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
# Edition-2024 members imply resolver 3; set it explicitly so features aren't
# over-unified across normal/build/dev deps (fewer features compiled).
resolver = "3"
members = ["beacon-api", "beacon-auth", "beacon-file-formats/beacon-arrow-netcdf", "beacon-file-formats/beacon-arrow-odv", "beacon-common", "beacon-config", "beacon-core", "beacon-functions", "beacon-data-lake", "beacon-sql-databases", "beacon-file-formats/beacon-delta", "beacon-file-formats/beacon-arrow-zarr", "beacon-file-formats/beacon-binary-format", "beacon-object-storage", "beacon-file-formats/beacon-nd-arrow", "beacon-datafusion-ext", "beacon-file-formats/beacon-iceberg", "beacon-file-formats/beacon-lance", "beacon-file-formats/beacon-nd-array", "beacon-file-formats/beacon-arrow-tiff", "beacon-file-formats/beacon-arrow-atlas", "beacon-file-formats/beacon-arrow-geoparquet", "beacon-file-formats/beacon-arrow-bbf", "beacon-file-formats/beacon-arrow-ipc", "beacon-file-formats/beacon-arrow-csv", "beacon-file-formats/beacon-arrow-parquet"]
members = ["beacon-api", "beacon-mcp", "beacon-auth", "beacon-file-formats/beacon-arrow-netcdf", "beacon-file-formats/beacon-arrow-odv", "beacon-common", "beacon-config", "beacon-core", "beacon-functions", "beacon-data-lake", "beacon-sql-databases", "beacon-file-formats/beacon-delta", "beacon-file-formats/beacon-arrow-zarr", "beacon-file-formats/beacon-binary-format", "beacon-object-storage", "beacon-file-formats/beacon-nd-arrow", "beacon-datafusion-ext", "beacon-file-formats/beacon-iceberg", "beacon-file-formats/beacon-lance", "beacon-file-formats/beacon-nd-array", "beacon-file-formats/beacon-arrow-tiff", "beacon-file-formats/beacon-arrow-atlas", "beacon-file-formats/beacon-arrow-geoparquet", "beacon-file-formats/beacon-arrow-bbf", "beacon-file-formats/beacon-arrow-ipc", "beacon-file-formats/beacon-arrow-csv", "beacon-file-formats/beacon-arrow-parquet"]
exclude = ["beacon-file-formats/beacon-binary-format-toolbox"]

[workspace.dependencies]
Expand Down
3 changes: 2 additions & 1 deletion beacon-api/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ prost = {workspace = true}
# Local dependencies
beacon-config = { path = "../beacon-config" }
beacon-core = { path = "../beacon-core" }
beacon-mcp = { path = "../beacon-mcp" }

[dev-dependencies]
# Enable beacon-core's test-only helper (ephemeral in-memory auth runtime) for the
Expand All @@ -53,4 +54,4 @@ beacon-core = { path = "../beacon-core", features = ["test-util"] }
# `tower::ServiceExt::oneshot` for driving the axum router in the HTTP auth tests.
tower = { version = "0.5", features = ["util"] }
# Temp storage roots for the admin dataset file-management HTTP tests.
tempfile = { workspace = true }
tempfile = { workspace = true }
63 changes: 63 additions & 0 deletions beacon-api/src/axum/admin/extensions.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
//! Admin endpoints for managing a table's downstream extensions.

use std::sync::Arc;

use ::axum::{
extract::{Path, State},
http::StatusCode,
Json,
};
use beacon_core::api::TableExtensions;
use beacon_core::runtime::Runtime;

use super::bad_request;

/// Replaces the named table's extensions document (MCP descriptor, query
/// presets). The document is validated against the table schema; an empty body
/// (`{}`) clears all extensions.
#[tracing::instrument(level = "info", skip(state, extensions))]
#[utoipa::path(
tag = "admin",
put,
path = "/api/admin/table-extensions/{table_name}",
params(("table_name" = String, Path, description = "Registered table name")),
request_body = TableExtensions,
responses(
(status = 200, description = "Extensions updated"),
(status = 400, description = "Invalid request, validation failed, or table not found")
),
security(("basic-auth" = []))
)]
pub(crate) async fn set_table_extensions(
State(state): State<Arc<Runtime>>,
Path(table_name): Path<String>,
Json(extensions): Json<TableExtensions>,
) -> Result<(), (StatusCode, String)> {
state
.set_table_extensions(table_name, extensions)
.await
.map_err(bad_request)
}

/// Removes all extensions from the named table.
#[tracing::instrument(level = "info", skip(state))]
#[utoipa::path(
tag = "admin",
delete,
path = "/api/admin/table-extensions/{table_name}",
params(("table_name" = String, Path, description = "Registered table name")),
responses(
(status = 200, description = "Extensions removed"),
(status = 400, description = "Table not found or removal failed")
),
security(("basic-auth" = []))
)]
pub(crate) async fn delete_table_extensions(
State(state): State<Arc<Runtime>>,
Path(table_name): Path<String>,
) -> Result<(), (StatusCode, String)> {
state
.delete_table_extensions(table_name)
.await
.map_err(bad_request)
}
5 changes: 5 additions & 0 deletions beacon-api/src/axum/admin/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ mod auth;
mod check;
mod crawlers;
mod datasets;
mod extensions;
mod external_tables;
mod tables;

Expand Down Expand Up @@ -51,6 +52,10 @@ pub(crate) fn setup_admin_router() -> (Router<Arc<Runtime>>, utoipa::openapi::Op
.routes(routes!(tables::list_table_config))
.routes(routes!(auth::list_users))
.routes(routes!(auth::list_roles))
.routes(routes!(
extensions::set_table_extensions,
extensions::delete_table_extensions
))
.split_for_parts();

(admin_router, admin_api)
Expand Down
1 change: 1 addition & 0 deletions beacon-api/src/axum/client/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ pub(crate) fn setup_client_router() -> (Router<Arc<Runtime>>, utoipa::openapi::O
.routes(routes!(tables::list_tables_with_schema))
.routes(routes!(tables::default_table))
.routes(routes!(tables::list_table_schema))
.routes(routes!(tables::list_table_extensions))
.routes(routes!(tables::default_table_schema))
.routes(routes!(functions::list_functions))
.routes(routes!(functions::list_table_functions))
Expand Down
44 changes: 43 additions & 1 deletion beacon-api/src/axum/client/tables.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ use ::axum::{
http::StatusCode,
Json,
};
use beacon_core::api::{SchemaFieldView, SchemaView};
use beacon_core::api::{SchemaFieldView, SchemaView, TableExtensions};
use beacon_core::runtime::Runtime;
use utoipa::{IntoParams, ToSchema};

Expand Down Expand Up @@ -110,6 +110,48 @@ pub(crate) async fn list_table_schema(
}
}

/// Query parameters for [`list_table_extensions`].
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, ToSchema, IntoParams)]
pub struct ListTableExtensionsQuery {
/// Name of the registered table whose extensions to return.
pub table_name: String,
}

/// Returns the downstream extensions (MCP descriptor, query presets) attached to
/// the named table, or 404 if the table is not registered. A table with no
/// extensions returns an empty object.
#[tracing::instrument(level = "info", skip(state))]
#[utoipa::path(
tag = "tables",
get,
path = "/api/table-extensions",
params(ListTableExtensionsQuery),
responses(
(status = 200, description = "The table's extensions", body = TableExtensions),
(status = 404, description = "Table not found"),
),
security(
(),
("basic-auth" = []),
("bearer" = [])
)
)]
pub(crate) async fn list_table_extensions(
State(state): State<Arc<Runtime>>,
Query(query): Query<ListTableExtensionsQuery>,
) -> Result<Json<TableExtensions>, (StatusCode, String)> {
match state.get_table_extensions(query.table_name.clone()).await {
Ok(extensions) => Ok(Json(extensions)),
Err(error) => {
tracing::error!(?error, "error listing table extensions");
Err((
StatusCode::NOT_FOUND,
format!("Table {} not found", query.table_name),
))
}
}
}

/// Returns the Arrow schema of the runtime's default table.
#[tracing::instrument(level = "info", skip(state))]
#[utoipa::path(
Expand Down
23 changes: 21 additions & 2 deletions beacon-api/src/axum/router.rs
Original file line number Diff line number Diff line change
Expand Up @@ -72,8 +72,27 @@ pub(crate) fn setup_router(
// lands on the UI instead of the API docs.
let web_ui = web_ui_router(&config.server.web_ui_dir);

let mut router = client_router
.merge(admin_router)
// MCP streamable-HTTP endpoint, gated by BEACON_MCP_ENABLED (default on). It
// rides the same `resolve_identity` middleware as the client API, so MCP tool
// calls execute under the caller's identity (or the anonymous principal when
// enabled) and per-user RBAC applies at query time.
let mcp_enabled = std::env::var("BEACON_MCP_ENABLED")
.map(|v| !matches!(v.trim().to_ascii_lowercase().as_str(), "false" | "0" | "off"))
.unwrap_or(true);
let mut router = client_router.merge(admin_router);
if mcp_enabled {
let mcp_router = ::axum::Router::new()
.route_service(
"/mcp",
beacon_mcp::streamable_http_service(beacon_runtime.clone()),
)
.layer(::axum::middleware::from_fn_with_state(
beacon_runtime.clone(),
crate::axum::auth::resolve_identity,
));
router = router.merge(mcp_router);
}
let mut router = router
.merge(Scalar::with_url("/scalar/", docs.clone()))
.route(
"/scalar",
Expand Down
Loading
Loading