mirror of
https://github.com/bitcoinresearchkit/brk.git
synced 2026-07-31 12:43:06 -07:00
server: use etag for vecs instead of date modified
This commit is contained in:
@@ -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, ¶ms.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) => {
|
||||
|
||||
@@ -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)]
|
||||
|
||||
@@ -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())
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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() {
|
||||
|
||||
Reference in New Issue
Block a user