server: use etag for vecs instead of date modified

This commit is contained in:
nym21
2025-06-24 10:11:03 +02:00
parent 0f3c267a48
commit 016d80e002
45 changed files with 132 additions and 181 deletions
+8 -21
View File
@@ -9,7 +9,7 @@ use brk_interface::{Format, Output, Params};
use brk_vec::{CollectableVec, StoredVec};
use color_eyre::eyre::eyre;
use crate::traits::{HeaderMapExtended, ModifiedState, ResponseExtended};
use crate::traits::{HeaderMapExtended, ResponseExtended};
use super::AppState;
@@ -63,24 +63,13 @@ fn req_to_response_res(
return Err(eyre!("Request is too heavy, max weight is {MAX_WEIGHT}"));
}
let mut date_modified_opt = None;
let etag = vecs.first().unwrap().1.etag(to);
if to.is_none() {
let not_modified = vecs
.iter()
.map(|(_, vec)| headers.check_if_modified_since_(vec.modified_time()?))
.all(|res| {
res.ok().is_some_and(|(modified, date_modified)| {
if date_modified_opt.is_none_or(|dm| dm > date_modified) {
date_modified_opt.replace(date_modified);
}
modified == ModifiedState::NotModifiedSince
})
});
if not_modified {
return Ok(Response::new_not_modified());
}
if headers
.get_if_none_match()
.is_some_and(|prev_etag| etag == prev_etag)
{
return Ok(Response::new_not_modified());
}
let output = interface.format(vecs, &params.rest)?;
@@ -100,9 +89,7 @@ fn req_to_response_res(
headers.insert_cors();
if let Some(date_modified) = date_modified_opt {
headers.insert_last_modified(date_modified);
}
headers.insert_etag(&etag);
match format {
Some(format) => {
+1 -2
View File
@@ -24,6 +24,7 @@ use brk_computer::Computer;
use brk_core::dot_brk_path;
use brk_indexer::Indexer;
use brk_interface::Interface;
use brk_mcp::route::MCPRoutes;
use color_eyre::owo_colors::OwoColorize;
use files::FilesRoutes;
use log::{error, info};
@@ -32,11 +33,9 @@ use tower_http::{compression::CompressionLayer, trace::TraceLayer};
mod api;
mod files;
mod mcp;
mod traits;
pub use files::Website;
use mcp::*;
use tracing::Span;
#[derive(Clone)]
-186
View File
@@ -1,186 +0,0 @@
use brk_interface::{IdParam, Interface, PaginatedIndexParam, PaginationParam, Params};
use brk_rmcp::{
Error as McpError, RoleServer, ServerHandler,
model::{
CallToolResult, Content, Implementation, InitializeRequestParam, InitializeResult,
ProtocolVersion, ServerCapabilities, ServerInfo,
},
service::RequestContext,
tool,
};
use log::info;
#[derive(Clone)]
pub struct API {
interface: &'static Interface<'static>,
}
const VERSION: &str = env!("CARGO_PKG_VERSION");
#[tool(tool_box)]
impl API {
pub fn new(interface: &'static Interface<'static>) -> Self {
Self { interface }
}
#[tool(description = "
Get the count of all existing indexes.
")]
async fn get_index_count(&self) -> Result<CallToolResult, McpError> {
info!("mcp: get_index_count");
Ok(CallToolResult::success(vec![
Content::json(self.interface.get_index_count()).unwrap(),
]))
}
#[tool(description = "
Get the count of all existing vec ids.
")]
async fn get_vecid_count(&self) -> Result<CallToolResult, McpError> {
info!("mcp: get_vecid_count");
Ok(CallToolResult::success(vec![
Content::json(self.interface.get_vecid_count()).unwrap(),
]))
}
#[tool(description = "
Get the count of all existing vecs.
Equals to the sum of supported Indexes of each vec id.
")]
async fn get_variant_count(&self) -> Result<CallToolResult, McpError> {
info!("mcp: get_variant_count");
Ok(CallToolResult::success(vec![
Content::json(self.interface.get_vec_count()).unwrap(),
]))
}
#[tool(description = "
Get the list of all existing indexes.
")]
async fn get_indexes(&self) -> Result<CallToolResult, McpError> {
info!("mcp: get_indexes");
Ok(CallToolResult::success(vec![
Content::json(self.interface.get_indexes()).unwrap(),
]))
}
#[tool(description = "
Get an object which has all existing indexes as keys and a list of their accepted variants as values.
")]
async fn get_accepted_indexes(&self) -> Result<CallToolResult, McpError> {
info!("mcp: get_accepted_indexes");
Ok(CallToolResult::success(vec![
Content::json(self.interface.get_accepted_indexes()).unwrap(),
]))
}
#[tool(description = "
Get a paginated list of all existing vec ids.
There are up to 1,000 values per page.
If the `page` param is omitted, it will default to page `0`.
")]
async fn get_vecids(
&self,
#[tool(aggr)] pagination: PaginationParam,
) -> Result<CallToolResult, McpError> {
info!("mcp: get_vecids");
Ok(CallToolResult::success(vec![
Content::json(self.interface.get_vecids(pagination)).unwrap(),
]))
}
#[tool(description = "
Get a paginated list of all vec ids which support a given index.
There are up to 1,000 values per page.
If the `page` param is omitted, it will default to page `0`.
")]
async fn get_index_to_vecids(
&self,
#[tool(aggr)] paginated_index: PaginatedIndexParam,
) -> Result<CallToolResult, McpError> {
info!("mcp: get_index_to_vecids");
Ok(CallToolResult::success(vec![
Content::json(self.interface.get_index_to_vecids(paginated_index)).unwrap(),
]))
}
#[tool(description = "
Get a list of all indexes supported by a given vec id.
The list will be empty if the vec id isn't correct.
")]
async fn get_vecid_to_indexes(
&self,
#[tool(aggr)] param: IdParam,
) -> Result<CallToolResult, McpError> {
info!("mcp: get_vecid_to_indexes");
Ok(CallToolResult::success(vec![
Content::json(self.interface.get_vecid_to_indexes(param.id)).unwrap(),
]))
}
#[tool(description = "
Get one or multiple vecs depending on given parameters.
If you'd like to request multiple vec ids, simply separate them with a ','.
To get the last value set `-1` to the `from` parameter.
The response's format will depend on the given parameters, it will be:
- A value: If requested only one vec and the given range returns one value (for example: `from=-1`)
- A list: If requested only one vec and the given range returns multiple values (for example: `from=-1000&count=100` or `from=-444&to=-333`)
- A matrix: When multiple vecs are requested, even if they each return one value.
")]
fn get_vecs(&self, #[tool(aggr)] params: Params) -> Result<CallToolResult, McpError> {
info!("mcp: get_vecs");
Ok(CallToolResult::success(vec![
Content::json(self.interface.search_and_format(params).unwrap()).unwrap(),
]))
}
#[tool(description = "
Get the running version of the Bitcoin Research Kit
")]
async fn get_version(&self) -> Result<CallToolResult, McpError> {
info!("mcp: get_version");
Ok(CallToolResult::success(vec![Content::text(format!(
"v{VERSION}"
))]))
}
}
#[tool(tool_box)]
impl ServerHandler for API {
fn get_info(&self) -> ServerInfo {
ServerInfo {
protocol_version: ProtocolVersion::LATEST,
capabilities: ServerCapabilities::builder().enable_tools().build(),
server_info: Implementation::from_build_env(),
instructions: Some(
"
This server provides an interface to communicate with a running instance of the Bitcoin Research Kit (also called brk or BRK).
Multiple tools are at your disposal including ones to fetch all sorts of Bitcoin on-chain metrics and market prices.
If you're unsure which datasets are available, try out different tools before browsing the web. Each tool gives important information about BRK's capabilities.
Vectors can also be called 'Vecs', 'Arrays' or 'Datasets', they can all be used interchangeably.
An 'Index' (or indexes) is the timeframe of a dataset.
'VecId' (or vecids) are the name of the dataset and what it represents.
"
.to_string(),
),
}
}
async fn initialize(
&self,
_request: InitializeRequestParam,
context: RequestContext<RoleServer>,
) -> Result<InitializeResult, McpError> {
if let Some(http_request_part) = context.extensions.get::<axum::http::request::Parts>() {
let initialize_headers = &http_request_part.headers;
let initialize_uri = &http_request_part.uri;
tracing::info!(?initialize_headers, %initialize_uri, "initialize from http server");
}
Ok(self.get_info())
}
}
-39
View File
@@ -1,39 +0,0 @@
use axum::Router;
use brk_interface::Interface;
use brk_rmcp::transport::{
StreamableHttpServerConfig,
streamable_http_server::{StreamableHttpService, session::local::LocalSessionManager},
};
mod api;
use api::*;
use log::info;
use crate::AppState;
pub trait MCPRoutes {
fn add_mcp_routes(self, interface: &'static Interface<'static>, mcp: bool) -> Self;
}
impl MCPRoutes for Router<AppState> {
fn add_mcp_routes(self, interface: &'static Interface<'static>, mcp: bool) -> Self {
if !mcp {
return self;
}
let config = StreamableHttpServerConfig {
// stateful_mode: false, // breaks Claude
..Default::default()
};
let service = StreamableHttpService::new(
move || Ok(API::new(interface)),
LocalSessionManager::default().into(),
config,
);
info!("Setting MCP...");
self.nest_service("/mcp", service)
}
}
+12 -1
View File
@@ -5,7 +5,7 @@ use std::{
use axum::http::{
HeaderMap,
header::{self, IF_MODIFIED_SINCE},
header::{self, IF_MODIFIED_SINCE, IF_NONE_MATCH},
};
use jiff::{Timestamp, civil::DateTime, fmt::strtime, tz::TimeZone};
use log::info;
@@ -21,6 +21,8 @@ pub enum ModifiedState {
pub trait HeaderMapExtended {
fn insert_cors(&mut self);
fn get_if_none_match(&self) -> Option<&str>;
fn get_if_modified_since(&self) -> Option<DateTime>;
fn check_if_modified_since(&self, path: &Path)
-> color_eyre::Result<(ModifiedState, DateTime)>;
@@ -31,6 +33,7 @@ pub trait HeaderMapExtended {
fn insert_cache_control_must_revalidate(&mut self);
fn insert_cache_control_immutable(&mut self);
fn insert_etag(&mut self, etag: &str);
fn insert_last_modified(&mut self, date: DateTime);
fn insert_content_disposition_attachment(&mut self);
@@ -85,6 +88,10 @@ impl HeaderMapExtended for HeaderMap {
self.insert(header::LAST_MODIFIED, formatted.parse().unwrap());
}
fn insert_etag(&mut self, etag: &str) {
self.insert(header::ETAG, etag.parse().unwrap());
}
fn check_if_modified_since(
&self,
path: &Path,
@@ -127,6 +134,10 @@ impl HeaderMapExtended for HeaderMap {
None
}
fn get_if_none_match(&self) -> Option<&str> {
self.get(IF_NONE_MATCH).and_then(|v| v.to_str().ok())
}
// https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/MIME_types/Common_types
fn insert_content_type(&mut self, path: &Path) {
match path.extension().unwrap().to_str().unwrap() {