mcp: part 2

This commit is contained in:
nym21
2025-06-21 20:34:14 +02:00
parent c3ae3cb768
commit c0f4ece17b
21 changed files with 280 additions and 187 deletions
Generated
+60 -44
View File
@@ -585,11 +585,11 @@ dependencies = [
"brk_computer", "brk_computer",
"brk_core", "brk_core",
"brk_indexer", "brk_indexer",
"brk_rmcp",
"brk_vec", "brk_vec",
"color-eyre", "color-eyre",
"derive_deref", "derive_deref",
"rmcp", "schemars 1.0.0-rc.2",
"schemars",
"serde", "serde",
"serde_json", "serde_json",
"serde_with", "serde_with",
@@ -621,6 +621,48 @@ dependencies = [
"zerocopy", "zerocopy",
] ]
[[package]]
name = "brk_rmcp"
version = "0.1.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "90d257949059da288c47b4f88ac80abcaa18afe65972d8b543369ee2099b37db"
dependencies = [
"base64 0.22.1",
"brk_rmcp-macros",
"bytes",
"chrono",
"futures",
"http",
"http-body",
"http-body-util",
"paste",
"pin-project-lite",
"rand 0.9.1",
"schemars 1.0.0-rc.2",
"serde",
"serde_json",
"sse-stream",
"thiserror 2.0.12",
"tokio",
"tokio-stream",
"tokio-util",
"tower-service",
"tracing",
"uuid",
]
[[package]]
name = "brk_rmcp-macros"
version = "0.1.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "12869ae1128aad2e0777db2688e661a55d389ecfb863936145ea1b789e0c2d34"
dependencies = [
"proc-macro2",
"quote",
"serde_json",
"syn 2.0.103",
]
[[package]] [[package]]
name = "brk_rolldown" name = "brk_rolldown"
version = "0.0.1" version = "0.0.1"
@@ -941,6 +983,7 @@ dependencies = [
"brk_interface", "brk_interface",
"brk_logger", "brk_logger",
"brk_parser", "brk_parser",
"brk_rmcp",
"brk_vec", "brk_vec",
"clap", "clap",
"clap_derive", "clap_derive",
@@ -948,7 +991,6 @@ dependencies = [
"jiff", "jiff",
"log", "log",
"minreq", "minreq",
"rmcp",
"serde", "serde",
"tokio", "tokio",
"tower-http", "tower-http",
@@ -3402,44 +3444,6 @@ dependencies = [
"libc", "libc",
] ]
[[package]]
name = "rmcp"
version = "0.1.6"
dependencies = [
"base64 0.22.1",
"bytes",
"chrono",
"futures",
"http",
"http-body",
"http-body-util",
"paste",
"pin-project-lite",
"rand 0.9.1",
"rmcp-macros",
"schemars",
"serde",
"serde_json",
"sse-stream",
"thiserror 2.0.12",
"tokio",
"tokio-stream",
"tokio-util",
"tower-service",
"tracing",
"uuid",
]
[[package]]
name = "rmcp-macros"
version = "0.1.6"
dependencies = [
"proc-macro2",
"quote",
"serde_json",
"syn 2.0.103",
]
[[package]] [[package]]
name = "rolldown-ariadne" name = "rolldown-ariadne"
version = "0.5.2" version = "0.5.2"
@@ -3539,6 +3543,18 @@ name = "schemars"
version = "0.9.0" version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4cd191f9397d57d581cddd31014772520aa448f65ef991055d7f61582c65165f" checksum = "4cd191f9397d57d581cddd31014772520aa448f65ef991055d7f61582c65165f"
dependencies = [
"dyn-clone",
"ref-cast",
"serde",
"serde_json",
]
[[package]]
name = "schemars"
version = "1.0.0-rc.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "284cf8058b5165c2e1aedb8f56f4b09a838a764d1c110178e1747d1f77a01ceb"
dependencies = [ dependencies = [
"dyn-clone", "dyn-clone",
"ref-cast", "ref-cast",
@@ -3549,9 +3565,9 @@ dependencies = [
[[package]] [[package]]
name = "schemars_derive" name = "schemars_derive"
version = "0.9.0" version = "1.0.0-rc.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5016d94c77c6d32f0b8e08b781f7dc8a90c2007d4e77472cc2807bc10a8438fe" checksum = "3ce51e6bea03670e88edba2d724eed817b97f4ab017a1001bae5da325ce93fe7"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
@@ -3713,7 +3729,7 @@ dependencies = [
"hex", "hex",
"indexmap 1.9.3", "indexmap 1.9.3",
"indexmap 2.9.0", "indexmap 2.9.0",
"schemars", "schemars 0.9.0",
"serde", "serde",
"serde_derive", "serde_derive",
"serde_json", "serde_json",
+3 -5
View File
@@ -29,9 +29,10 @@ brk_core = { version = "0.0.66", path = "crates/brk_core" }
brk_exit = { version = "0.0.66", path = "crates/brk_exit" } brk_exit = { version = "0.0.66", path = "crates/brk_exit" }
brk_fetcher = { version = "0.0.66", path = "crates/brk_fetcher" } brk_fetcher = { version = "0.0.66", path = "crates/brk_fetcher" }
brk_indexer = { version = "0.0.66", path = "crates/brk_indexer" } brk_indexer = { version = "0.0.66", path = "crates/brk_indexer" }
brk_interface = { version = "0.0.66", path = "crates/brk_interface" }
brk_logger = { version = "0.0.66", path = "crates/brk_logger" } brk_logger = { version = "0.0.66", path = "crates/brk_logger" }
brk_parser = { version = "0.0.66", path = "crates/brk_parser" } brk_parser = { version = "0.0.66", path = "crates/brk_parser" }
brk_interface = { version = "0.0.66", path = "crates/brk_interface" } brk_rmcp = { version = "0.1.6", features = ["transport-streamable-http-server", "transport-worker"]}
brk_server = { version = "0.0.66", path = "crates/brk_server" } brk_server = { version = "0.0.66", path = "crates/brk_server" }
brk_state = { version = "0.0.66", path = "crates/brk_state" } brk_state = { version = "0.0.66", path = "crates/brk_state" }
brk_store = { version = "0.0.66", path = "crates/brk_store" } brk_store = { version = "0.0.66", path = "crates/brk_store" }
@@ -46,10 +47,7 @@ jiff = "0.2.15"
log = { version = "0.4.27" } log = { version = "0.4.27" }
minreq = { version = "2.13.4", features = ["https", "serde_json"] } minreq = { version = "2.13.4", features = ["https", "serde_json"] }
rayon = "1.10.0" rayon = "1.10.0"
rmcp = { path = "../rust-sdk/crates/rmcp", features = ["transport-streamable-http-server", "transport-worker"] } schemars = "1.0.0-rc.2"
schemars = "0.9.0"
# rmcp = { git = "https://github.com/modelcontextprotocol/rust-sdk", branch = "main" , features = ["transport-streamable-http-server", "transport-worker"] }
# schemars = "0.8.0"
serde = { version = "1.0.219" } serde = { version = "1.0.219" }
serde_bytes = "0.11.17" serde_bytes = "0.11.17"
serde_derive = "1.0.219" serde_derive = "1.0.219"
+14 -1
View File
@@ -87,7 +87,12 @@ pub struct Config {
#[arg(long, value_name = "SECONDS")] #[arg(long, value_name = "SECONDS")]
delay: Option<u64>, delay: Option<u64>,
/// DEV: Activate to watch the selected website's folder for changes, default: false, saved /// Activate the Model Context Protocol (MCP) endpoint to give LLMs access to BRK, default: false, saved
#[serde(default, deserialize_with = "default_on_error")]
#[arg(long, value_name = "BOOL")]
mcp: Option<bool>,
/// DEV: Activate watching the selected website's folder for changes, default: false, saved
#[serde(default, deserialize_with = "default_on_error")] #[serde(default, deserialize_with = "default_on_error")]
#[arg(long, value_name = "BOOL")] #[arg(long, value_name = "BOOL")]
watch: Option<bool>, watch: Option<bool>,
@@ -171,6 +176,10 @@ impl Config {
config_saved.check_collisions = Some(check_collisions); config_saved.check_collisions = Some(check_collisions);
} }
if let Some(mcp) = config_args.mcp.take() {
config_saved.mcp = Some(mcp);
}
if let Some(watch) = config_args.watch.take() { if let Some(watch) = config_args.watch.take() {
config_saved.watch = Some(watch); config_saved.watch = Some(watch);
} }
@@ -356,6 +365,10 @@ impl Config {
self.check_collisions.is_some_and(|b| b) self.check_collisions.is_some_and(|b| b)
} }
pub fn mcp(&self) -> bool {
self.mcp.is_some_and(|b| b)
}
pub fn watch(&self) -> bool { pub fn watch(&self) -> bool {
self.watch.is_some_and(|b| b) self.watch.is_some_and(|b| b)
} }
+2 -1
View File
@@ -57,8 +57,9 @@ pub fn run() -> color_eyre::Result<()> {
let server = Server::new(served_indexer, served_computer, config.website())?; let server = Server::new(served_indexer, served_computer, config.website())?;
let watch = config.watch(); let watch = config.watch();
let mcp = config.mcp();
let opt = Some(tokio::spawn(async move { let opt = Some(tokio::spawn(async move {
server.serve(watch).await.unwrap(); server.serve(watch, mcp).await.unwrap();
})); }));
sleep(Duration::from_secs(1)); sleep(Duration::from_secs(1));
+1 -1
View File
@@ -14,7 +14,7 @@ brk_indexer = { workspace = true }
brk_vec = { workspace = true } brk_vec = { workspace = true }
color-eyre = { workspace = true } color-eyre = { workspace = true }
derive_deref = { workspace = true } derive_deref = { workspace = true }
rmcp = { workspace = true } brk_rmcp = { workspace = true }
schemars = { workspace = true } schemars = { workspace = true }
serde = { workspace = true } serde = { workspace = true }
serde_json = { workspace = true } serde_json = { workspace = true }
+1 -1
View File
@@ -1,5 +1,5 @@
use brk_rmcp::schemars::JsonSchema;
use color_eyre::eyre::eyre; use color_eyre::eyre::eyre;
use rmcp::schemars::JsonSchema;
use serde::Deserialize; use serde::Deserialize;
#[allow(clippy::upper_case_acronyms)] #[allow(clippy::upper_case_acronyms)]
+1
View File
@@ -165,6 +165,7 @@ impl<'de> Deserialize<'de> for Index {
{ {
let str = String::deserialize(deserializer)?; let str = String::deserialize(deserializer)?;
if let Ok(index) = Index::try_from(str.as_str()) { if let Ok(index) = Index::try_from(str.as_str()) {
// dbg!(index);
Ok(index) Ok(index)
} else { } else {
Err(Error::custom("Bad index")) Err(Error::custom("Bad index"))
+8 -12
View File
@@ -15,6 +15,7 @@ mod format;
mod index; mod index;
mod maybe_ids; mod maybe_ids;
mod output; mod output;
mod pagination;
mod params; mod params;
mod table; mod table;
mod vecs; mod vecs;
@@ -22,7 +23,8 @@ mod vecs;
pub use format::Format; pub use format::Format;
pub use index::Index; pub use index::Index;
pub use output::{Output, Value}; pub use output::{Output, Value};
pub use params::{Pagination, Params, ParamsOpt}; pub use pagination::{PaginatedIndexParam, PaginationParam};
pub use params::{IdParam, Params, ParamsOpt};
pub use table::Tabled; pub use table::Tabled;
use vecs::Vecs; use vecs::Vecs;
@@ -192,21 +194,15 @@ impl<'a> Interface<'a> {
&self.vecs.accepted_indexes &self.vecs.accepted_indexes
} }
pub fn get_vecids(&self, pagination: Pagination) -> &[&str] { pub fn get_vecids(&self, pagination: PaginationParam) -> &[&str] {
self.vecs.ids(pagination) self.vecs.ids(pagination)
} }
pub fn get_indexes_to_vecids( pub fn get_index_to_vecids(&self, paginated_index: PaginatedIndexParam) -> Vec<&str> {
&self, self.vecs.index_to_ids(paginated_index)
pagination: Pagination,
) -> BTreeMap<&'static str, Vec<&str>> {
self.vecs.indexes_to_ids(pagination)
} }
pub fn get_vecids_to_indexes( pub fn get_vecid_to_indexes(&self, id: String) -> Option<&Vec<&'static str>> {
&self, self.vecs.id_to_indexes(id)
pagination: Pagination,
) -> BTreeMap<&str, Vec<&'static str>> {
self.vecs.ids_to_indexes(pagination)
} }
} }
+12 -4
View File
@@ -22,9 +22,17 @@ impl<'de> Deserialize<'de> for MaybeIds {
where where
D: serde::Deserializer<'de>, D: serde::Deserializer<'de>,
{ {
let str = String::deserialize(deserializer)?; let maybe_ids = match serde_json::Value::deserialize(deserializer)? {
Ok(MaybeIds( serde_json::Value::String(str) => {
str.split(",").map(|s| s.to_string()).collect::<Vec<_>>(), str.split(",").map(|s| s.to_string()).collect::<Vec<_>>()
)) }
serde_json::Value::Array(vec) => vec
.into_iter()
.map(|s| s.as_str().unwrap().to_string())
.collect::<Vec<_>>(),
_ => return Err(serde::de::Error::custom("Bad ids format")),
};
// dbg!(&maybe_ids);
Ok(MaybeIds(maybe_ids))
} }
} }
+1
View File
@@ -6,6 +6,7 @@ use tabled::Tabled as TabledTabled;
use crate::Format; use crate::Format;
#[derive(Debug, Serialize)] #[derive(Debug, Serialize)]
#[serde(untagged)]
pub enum Output { pub enum Output {
Json(Value), Json(Value),
CSV(String), CSV(String),
+31
View File
@@ -0,0 +1,31 @@
use schemars::JsonSchema;
use serde::Deserialize;
use crate::Index;
#[derive(Debug, Default, Deserialize, JsonSchema)]
pub struct PaginationParam {
#[serde(alias = "p")]
#[schemars(description = "Pagination index")]
#[serde(default)]
pub page: usize,
}
impl PaginationParam {
const PER_PAGE: usize = 1_000;
pub fn start(&self, len: usize) -> usize {
(self.page * Self::PER_PAGE).clamp(0, len)
}
pub fn end(&self, len: usize) -> usize {
((self.page + 1) * Self::PER_PAGE).clamp(0, len)
}
}
#[derive(Debug, Deserialize, JsonSchema)]
pub struct PaginatedIndexParam {
pub index: Index,
#[serde(flatten)]
pub pagination: PaginationParam,
}
+8 -21
View File
@@ -1,6 +1,6 @@
use std::ops::Deref; use std::ops::Deref;
use rmcp::schemars::{self, JsonSchema}; use brk_rmcp::schemars::{self, JsonSchema};
use serde::{Deserialize, Deserializer}; use serde::{Deserialize, Deserializer};
use crate::{Format, Index, maybe_ids::MaybeIds}; use crate::{Format, Index, maybe_ids::MaybeIds};
@@ -52,6 +52,7 @@ pub struct ParamsOpt {
count: Option<usize>, count: Option<usize>,
/// Format of the output /// Format of the output
#[serde(default)]
format: Option<Format>, format: Option<Format>,
} }
@@ -85,7 +86,7 @@ impl ParamsOpt {
if let Some(c) = self.count { if let Some(c) = self.count {
let c = c as i64; let c = c as i64;
if let Some(f) = self.from { if let Some(f) = self.from {
if f.is_positive() || f.abs() > c { if f >= 0 || f.abs() > c {
return Some(f + c); return Some(f + c);
} }
} else { } else {
@@ -101,6 +102,11 @@ impl ParamsOpt {
} }
} }
#[derive(Debug, Deserialize, JsonSchema)]
pub struct IdParam {
pub id: String,
}
fn de_unquote_i64<'de, D>(deserializer: D) -> Result<Option<i64>, D::Error> fn de_unquote_i64<'de, D>(deserializer: D) -> Result<Option<i64>, D::Error>
where where
D: Deserializer<'de>, D: Deserializer<'de>,
@@ -154,22 +160,3 @@ where
} }
} }
} }
#[derive(Debug, Default, Deserialize, JsonSchema)]
pub struct Pagination {
#[serde(alias = "p")]
#[schemars(description = "Pagination index")]
pub page: usize,
}
impl Pagination {
const PER_PAGE: usize = 1_000;
pub fn start(&self, len: usize) -> usize {
(self.page * Self::PER_PAGE).clamp(0, len)
}
pub fn end(&self, len: usize) -> usize {
((self.page + 1) * Self::PER_PAGE).clamp(0, len)
}
}
+35 -39
View File
@@ -5,7 +5,7 @@ use brk_indexer::Indexer;
use brk_vec::AnyCollectableVec; use brk_vec::AnyCollectableVec;
use derive_deref::{Deref, DerefMut}; use derive_deref::{Deref, DerefMut};
use crate::params::Pagination; use crate::pagination::{PaginatedIndexParam, PaginationParam};
use super::index::Index; use super::index::Index;
@@ -19,8 +19,8 @@ pub struct Vecs<'a> {
pub index_count: usize, pub index_count: usize,
pub id_count: usize, pub id_count: usize,
pub vec_count: usize, pub vec_count: usize,
serialized_id_to_indexes: BTreeMap<&'a str, Vec<&'static str>>, id_to_indexes: BTreeMap<&'a str, Vec<&'static str>>,
serialized_indexes_to_ids: BTreeMap<&'static str, Vec<&'a str>>, indexes_to_ids: BTreeMap<Index, Vec<&'a str>>,
} }
impl<'a> Vecs<'a> { impl<'a> Vecs<'a> {
@@ -40,17 +40,22 @@ impl<'a> Vecs<'a> {
.for_each(|vec| this.insert(vec)); .for_each(|vec| this.insert(vec));
let mut ids = this.id_to_index_to_vec.keys().cloned().collect::<Vec<_>>(); let mut ids = this.id_to_index_to_vec.keys().cloned().collect::<Vec<_>>();
ids.sort_unstable_by(|a, b| {
let len_cmp = a.len().cmp(&b.len()); let sort_ids = |ids: &mut Vec<&str>| {
if len_cmp == std::cmp::Ordering::Equal { ids.sort_unstable_by(|a, b| {
a.cmp(b) let len_cmp = a.len().cmp(&b.len());
} else { if len_cmp == std::cmp::Ordering::Equal {
len_cmp a.cmp(b)
} } else {
}); len_cmp
}
})
};
sort_ids(&mut ids);
this.ids = ids; this.ids = ids;
this.id_count = this.index_to_id_to_vec.keys().count(); this.id_count = this.id_to_index_to_vec.keys().count();
this.index_count = this.index_to_id_to_vec.keys().count(); this.index_count = this.index_to_id_to_vec.keys().count();
this.vec_count = this this.vec_count = this
.index_to_id_to_vec .index_to_id_to_vec
@@ -67,7 +72,7 @@ impl<'a> Vecs<'a> {
.keys() .keys()
.map(|i| (i.serialize_long(), i.possible_values())) .map(|i| (i.serialize_long(), i.possible_values()))
.collect::<BTreeMap<_, _>>(); .collect::<BTreeMap<_, _>>();
this.serialized_id_to_indexes = this this.id_to_indexes = this
.id_to_index_to_vec .id_to_index_to_vec
.iter() .iter()
.map(|(id, index_to_vec)| { .map(|(id, index_to_vec)| {
@@ -80,16 +85,14 @@ impl<'a> Vecs<'a> {
) )
}) })
.collect(); .collect();
this.serialized_indexes_to_ids = this this.indexes_to_ids = this
.index_to_id_to_vec .index_to_id_to_vec
.iter() .iter()
.map(|(index, id_to_vec)| { .map(|(index, id_to_vec)| (*index, id_to_vec.keys().cloned().collect::<Vec<_>>()))
(
index.serialize_long(),
id_to_vec.keys().cloned().collect::<Vec<_>>(),
)
})
.collect(); .collect();
this.indexes_to_ids
.values_mut()
.for_each(|ids| sort_ids(ids));
this this
} }
@@ -150,35 +153,28 @@ impl<'a> Vecs<'a> {
} }
} }
pub fn ids(&self, pagination: Pagination) -> &[&'_ str] { pub fn ids(&self, pagination: PaginationParam) -> &[&'_ str] {
let len = self.ids.len(); let len = self.ids.len();
let start = pagination.start(len); let start = pagination.start(len);
let end = pagination.end(len); let end = pagination.end(len);
&self.ids[start..end] &self.ids[start..end]
} }
pub fn ids_to_indexes(&self, pagination: Pagination) -> BTreeMap<&'_ str, Vec<&'static str>> { pub fn id_to_indexes(&self, id: String) -> Option<&Vec<&'static str>> {
let len = self.serialized_id_to_indexes.len(); self.id_to_indexes.get(id.as_str())
let start = pagination.start(len);
let end = pagination.end(len);
self.serialized_id_to_indexes
.iter()
.skip(start)
.take(end)
.map(|(ids, indexes)| (*ids, indexes.clone()))
.collect()
} }
pub fn indexes_to_ids(&self, pagination: Pagination) -> BTreeMap<&'static str, Vec<&'a str>> { pub fn index_to_ids(
let len = self.serialized_indexes_to_ids.len(); &self,
PaginatedIndexParam { index, pagination }: PaginatedIndexParam,
) -> Vec<&'a str> {
let vec = self.indexes_to_ids.get(&index).unwrap();
let len = vec.len();
let start = pagination.start(len); let start = pagination.start(len);
let end = pagination.end(len); let end = pagination.end(len);
self.serialized_indexes_to_ids
.iter() vec.iter().skip(start).take(end).cloned().collect()
.skip(start)
.take(end)
.map(|(index, ids)| (*index, ids.clone()))
.collect()
} }
} }
+4 -4
View File
@@ -25,10 +25,9 @@ pub fn init(path: Option<&Path>) {
.unwrap() .unwrap()
}); });
Builder::from_env( Builder::from_env(Env::default().default_filter_or(
Env::default() "info,fjall=off,lsm_tree=off,rolldown=off,brk_rolldown=off,rmcp=off,brk_rmcp=off,tracing=off",
.default_filter_or("info,fjall=off,lsm_tree=off,rolldown=off,brk_rolldown=off"), ))
)
.format(move |buf, record| { .format(move |buf, record| {
let date_time = Timestamp::now() let date_time = Timestamp::now()
.to_zoned(tz::TimeZone::system()) .to_zoned(tz::TimeZone::system())
@@ -83,6 +82,7 @@ fn write(
args: impl Display, args: impl Display,
) -> Result<(), std::io::Error> { ) -> Result<(), std::io::Error> {
writeln!(buf, "{} {} {} {}", date_time, dash, level, args) writeln!(buf, "{} {} {} {}", date_time, dash, level, args)
// Don't remove, used to know the target of unwanted logs
// writeln!( // writeln!(
// buf, // buf,
// "{} {} {} {} {}", // "{} {} {} {} {}",
+1 -1
View File
@@ -26,7 +26,7 @@ color-eyre = { workspace = true }
jiff = { workspace = true } jiff = { workspace = true }
log = { workspace = true } log = { workspace = true }
minreq = { workspace = true } minreq = { workspace = true }
rmcp = { workspace = true } brk_rmcp = { workspace = true }
serde = { workspace = true } serde = { workspace = true }
tokio = { workspace = true } tokio = { workspace = true }
tower-http = { version = "0.6.6", features = ["compression-full", "trace"] } tower-http = { version = "0.6.6", features = ["compression-full", "trace"] }
+9 -6
View File
@@ -43,27 +43,30 @@ The API uses `brk_interface` and so inherites all of its features including form
#### [`GET /api/vecs/index-count`](https://bitcoinresearchkit.org/api/vecs/index-count) #### [`GET /api/vecs/index-count`](https://bitcoinresearchkit.org/api/vecs/index-count)
Count of all possible indexes Get the count of all existing indexes.
#### [`GET /api/vecs/id-count`](https://bitcoinresearchkit.org/api/vecs/id-count) #### [`GET /api/vecs/id-count`](https://bitcoinresearchkit.org/api/vecs/id-count)
Count of all possible ids Get the count of all existing vec ids.
#### [`GET /api/vecs/variant-count`](https://bitcoinresearchkit.org/api/vecs/variant-count) #### [`GET /api/vecs/variant-count`](https://bitcoinresearchkit.org/api/vecs/variant-count)
Count of all possible variants Get the count of all existing vecs. \
Equals to the sum of supported Indexes of each vec id.
#### [`GET /api/vecs/indexes`](https://bitcoinresearchkit.org/api/vecs/indexes) #### [`GET /api/vecs/indexes`](https://bitcoinresearchkit.org/api/vecs/indexes)
Get a list of all possible indexes Get the list of all existing indexes.
#### [`GET /api/vecs/accepted-indexes`](https://bitcoinresearchkit.org/api/vecs/accepted-indexes) #### [`GET /api/vecs/accepted-indexes`](https://bitcoinresearchkit.org/api/vecs/accepted-indexes)
Get a list of possible indexes and all their accepted variants Get an object which has all existing indexes as keys and a list of their accepted variants as values.
#### [`GET /api/vecs/ids`](https://bitcoinresearchkit.org/api/vecs/ids) #### [`GET /api/vecs/ids`](https://bitcoinresearchkit.org/api/vecs/ids)
A list of all possible vec ids 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`.
#### [`GET /api/vecs/variants`](https://bitcoinresearchkit.org/api/vecs/variants) #### [`GET /api/vecs/variants`](https://bitcoinresearchkit.org/api/vecs/variants)
+1 -1
View File
@@ -48,7 +48,7 @@ pub fn main() -> color_eyre::Result<()> {
let server = Server::new(served_indexer, served_computer, Website::Default)?; let server = Server::new(served_indexer, served_computer, Website::Default)?;
let server = tokio::spawn(async move { let server = tokio::spawn(async move {
server.serve(true).await.unwrap(); server.serve(true, true).await.unwrap();
}); });
if process { if process {
+20 -18
View File
@@ -5,7 +5,7 @@ use axum::{
response::{IntoResponse, Redirect, Response}, response::{IntoResponse, Redirect, Response},
routing::get, routing::get,
}; };
use brk_interface::{Index, Pagination, Params, ParamsOpt}; use brk_interface::{IdParam, Index, PaginatedIndexParam, PaginationParam, Params, ParamsOpt};
use super::AppState; use super::AppState;
@@ -18,7 +18,7 @@ pub trait ApiRoutes {
fn add_api_routes(self) -> Self; fn add_api_routes(self) -> Self;
} }
const TO_SEPARATOR: &str = "-to-"; const TO_SEPARATOR: &str = "_to_";
impl ApiRoutes for Router<AppState> { impl ApiRoutes for Router<AppState> {
fn add_api_routes(self) -> Self { fn add_api_routes(self) -> Self {
@@ -56,29 +56,29 @@ impl ApiRoutes for Router<AppState> {
"/api/vecs/ids", "/api/vecs/ids",
get( get(
async |State(app_state): State<AppState>, async |State(app_state): State<AppState>,
Query(pagination): Query<Pagination>| Query(pagination): Query<PaginationParam>|
-> Response { -> Response {
Json(app_state.interface.get_vecids(pagination)).into_response() Json(app_state.interface.get_vecids(pagination)).into_response()
}, },
), ),
) )
.route( .route(
"/api/vecs/indexes-to-ids", "/api/vecs/index-to-ids",
get( get(
async |State(app_state): State<AppState>, async |State(app_state): State<AppState>,
Query(pagination): Query<Pagination>| Query(paginated_index): Query<PaginatedIndexParam>|
-> Response { -> Response {
Json(app_state.interface.get_indexes_to_vecids(pagination)).into_response() Json(app_state.interface.get_index_to_vecids(paginated_index)).into_response()
}, },
), ),
) )
.route( .route(
"/api/vecs/ids-to-indexes", "/api/vecs/id-to-indexes",
get( get(
async |State(app_state): State<AppState>, async |State(app_state): State<AppState>,
Query(pagination): Query<Pagination>| Query(param): Query<IdParam>|
-> Response { -> Response {
Json(app_state.interface.get_vecids_to_indexes(pagination)).into_response() Json(app_state.interface.get_vecid_to_indexes(param.id)).into_response()
}, },
), ),
) )
@@ -92,16 +92,18 @@ impl ApiRoutes for Router<AppState> {
Query(params_opt): Query<ParamsOpt>, Query(params_opt): Query<ParamsOpt>,
state: State<AppState>| state: State<AppState>|
-> Response { -> Response {
let variant = variant.replace("_", "-"); let variant = variant.replace("-", "_");
let mut split = variant.split(TO_SEPARATOR); let mut split = variant.split(TO_SEPARATOR);
let params = Params::from((
( if let Ok(index) = Index::try_from(split.next().unwrap()) {
Index::try_from(split.next().unwrap()).unwrap(), let params = Params::from((
split.collect::<Vec<_>>().join(TO_SEPARATOR), (index, split.collect::<Vec<_>>().join(TO_SEPARATOR)),
), params_opt,
params_opt, ));
)); interface::handler(headers, Query(params), state).await
interface::handler(headers, Query(params), state).await } else {
"Bad variant".into_response()
}
}, },
), ),
) )
+2 -2
View File
@@ -113,7 +113,7 @@ impl Server {
})) }))
} }
pub async fn serve(self, watch: bool) -> color_eyre::Result<()> { pub async fn serve(self, watch: bool, mcp: bool) -> color_eyre::Result<()> {
let state = self.0; let state = self.0;
if let Some(websites_path) = state.websites_path.clone() { if let Some(websites_path) = state.websites_path.clone() {
@@ -162,7 +162,7 @@ impl Server {
let router = Router::new() let router = Router::new()
.add_api_routes() .add_api_routes()
.add_website_routes(state.website) .add_website_routes(state.website)
.add_mcp_routes(state.interface) .add_mcp_routes(state.interface, mcp)
.route("/version", get(Json(VERSION))) .route("/version", get(Json(VERSION)))
.with_state(state) .with_state(state)
.layer(compression_layer) .layer(compression_layer)
+55 -22
View File
@@ -1,5 +1,5 @@
use brk_interface::{Interface, Pagination, Params}; use brk_interface::{IdParam, Interface, PaginatedIndexParam, PaginationParam, Params};
use rmcp::{ use brk_rmcp::{
Error as McpError, RoleServer, ServerHandler, Error as McpError, RoleServer, ServerHandler,
model::{ model::{
CallToolResult, Content, Implementation, InitializeRequestParam, InitializeResult, CallToolResult, Content, Implementation, InitializeRequestParam, InitializeResult,
@@ -8,6 +8,7 @@ use rmcp::{
service::RequestContext, service::RequestContext,
tool, tool,
}; };
use log::info;
#[derive(Clone)] #[derive(Clone)]
pub struct API { pub struct API {
@@ -23,88 +24,111 @@ impl API {
} }
#[tool(description = " #[tool(description = "
Get the count of all existing indexes Get the count of all existing indexes.
")] ")]
async fn get_index_count(&self) -> Result<CallToolResult, McpError> { async fn get_index_count(&self) -> Result<CallToolResult, McpError> {
info!("mcp: get_index_count");
Ok(CallToolResult::success(vec![ Ok(CallToolResult::success(vec![
Content::json(self.interface.get_index_count()).unwrap(), Content::json(self.interface.get_index_count()).unwrap(),
])) ]))
} }
#[tool(description = " #[tool(description = "
Get the count of all existing vec ids Get the count of all existing vec ids.
")] ")]
async fn get_vecid_count(&self) -> Result<CallToolResult, McpError> { async fn get_vecid_count(&self) -> Result<CallToolResult, McpError> {
info!("mcp: get_vecid_count");
Ok(CallToolResult::success(vec![ Ok(CallToolResult::success(vec![
Content::json(self.interface.get_vecid_count()).unwrap(), Content::json(self.interface.get_vecid_count()).unwrap(),
])) ]))
} }
#[tool(description = " #[tool(description = "
Get the count of all existing vecs 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> { async fn get_variant_count(&self) -> Result<CallToolResult, McpError> {
info!("mcp: get_variant_count");
Ok(CallToolResult::success(vec![ Ok(CallToolResult::success(vec![
Content::json(self.interface.get_vec_count()).unwrap(), Content::json(self.interface.get_vec_count()).unwrap(),
])) ]))
} }
#[tool(description = " #[tool(description = "
Get the list of all existing indexes Get the list of all existing indexes.
")] ")]
async fn get_indexes(&self) -> Result<CallToolResult, McpError> { async fn get_indexes(&self) -> Result<CallToolResult, McpError> {
info!("mcp: get_indexes");
Ok(CallToolResult::success(vec![ Ok(CallToolResult::success(vec![
Content::json(self.interface.get_indexes()).unwrap(), Content::json(self.interface.get_indexes()).unwrap(),
])) ]))
} }
#[tool(description = " #[tool(description = "
Get an object which has all existing indexes as keys and a list of their accepted variants as values 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> { async fn get_accepted_indexes(&self) -> Result<CallToolResult, McpError> {
info!("mcp: get_accepted_indexes");
Ok(CallToolResult::success(vec![ Ok(CallToolResult::success(vec![
Content::json(self.interface.get_accepted_indexes()).unwrap(), Content::json(self.interface.get_accepted_indexes()).unwrap(),
])) ]))
} }
#[tool(description = " #[tool(description = "
Get the list of all existing vec ids 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( async fn get_vecids(
&self, &self,
#[tool(aggr)] pagination: Pagination, #[tool(aggr)] pagination: PaginationParam,
) -> Result<CallToolResult, McpError> { ) -> Result<CallToolResult, McpError> {
info!("mcp: get_vecids");
Ok(CallToolResult::success(vec![ Ok(CallToolResult::success(vec![
Content::json(self.interface.get_vecids(pagination)).unwrap(), Content::json(self.interface.get_vecids(pagination)).unwrap(),
])) ]))
} }
#[tool(description = " #[tool(description = "
Get an object which has all existing indexes as keys and a list of ids of vecs which support that index as values 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_indexes_to_vecids( async fn get_index_to_vecids(
&self, &self,
#[tool(aggr)] pagination: Pagination, #[tool(aggr)] paginated_index: PaginatedIndexParam,
) -> Result<CallToolResult, McpError> { ) -> Result<CallToolResult, McpError> {
info!("mcp: get_index_to_vecids");
Ok(CallToolResult::success(vec![ Ok(CallToolResult::success(vec![
Content::json(self.interface.get_indexes_to_vecids(pagination)).unwrap(), Content::json(self.interface.get_index_to_vecids(paginated_index)).unwrap(),
])) ]))
} }
#[tool(description = " #[tool(description = "
Get an object which has all existing vec ids as keys and a list of indexes supported by that vec id as values 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_vecids_to_indexes( async fn get_vecid_to_indexes(
&self, &self,
#[tool(aggr)] pagination: Pagination, #[tool(aggr)] param: IdParam,
) -> Result<CallToolResult, McpError> { ) -> Result<CallToolResult, McpError> {
info!("mcp: get_vecid_to_indexes");
Ok(CallToolResult::success(vec![ Ok(CallToolResult::success(vec![
Content::json(self.interface.get_vecids_to_indexes(pagination)).unwrap(), Content::json(self.interface.get_vecid_to_indexes(param.id)).unwrap(),
])) ]))
} }
#[tool(description = "Get one or multiple vecs depending on given parameters")] #[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> { fn get_vecs(&self, #[tool(aggr)] params: Params) -> Result<CallToolResult, McpError> {
info!("mcp: get_vecs");
Ok(CallToolResult::success(vec![ Ok(CallToolResult::success(vec![
Content::json(self.interface.search_and_format(params).unwrap()).unwrap(), Content::json(self.interface.search_and_format(params).unwrap()).unwrap(),
])) ]))
@@ -114,6 +138,7 @@ Get an object which has all existing vec ids as keys and a list of indexes suppo
Get the running version of the Bitcoin Research Kit Get the running version of the Bitcoin Research Kit
")] ")]
async fn get_version(&self) -> Result<CallToolResult, McpError> { async fn get_version(&self) -> Result<CallToolResult, McpError> {
info!("mcp: get_version");
Ok(CallToolResult::success(vec![Content::text(format!( Ok(CallToolResult::success(vec![Content::text(format!(
"v{VERSION}" "v{VERSION}"
))])) ))]))
@@ -124,14 +149,22 @@ Get the running version of the Bitcoin Research Kit
impl ServerHandler for API { impl ServerHandler for API {
fn get_info(&self) -> ServerInfo { fn get_info(&self) -> ServerInfo {
ServerInfo { ServerInfo {
protocol_version: ProtocolVersion::V_2025_03_26, protocol_version: ProtocolVersion::LATEST,
capabilities: ServerCapabilities::builder().enable_tools().build(), capabilities: ServerCapabilities::builder().enable_tools().build(),
server_info: Implementation::from_build_env(), server_info: Implementation::from_build_env(),
instructions: Some( instructions: Some(
" "
This server provides an interface to communicate with a running instance of the Bitcoin Research Kit (brk). 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 data.
Arrays are also called Vectors (or Vecs). 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(), .to_string(),
), ),
+11 -4
View File
@@ -1,23 +1,28 @@
use axum::Router; use axum::Router;
use brk_interface::Interface; use brk_interface::Interface;
use rmcp::transport::{ use brk_rmcp::transport::{
StreamableHttpServerConfig, StreamableHttpServerConfig,
streamable_http_server::{StreamableHttpService, session::local::LocalSessionManager}, streamable_http_server::{StreamableHttpService, session::local::LocalSessionManager},
}; };
mod api; mod api;
use api::*; use api::*;
use log::info;
use crate::AppState; use crate::AppState;
pub trait MCPRoutes { pub trait MCPRoutes {
fn add_mcp_routes(self, interface: &'static Interface<'static>) -> Self; fn add_mcp_routes(self, interface: &'static Interface<'static>, mcp: bool) -> Self;
} }
impl MCPRoutes for Router<AppState> { impl MCPRoutes for Router<AppState> {
fn add_mcp_routes(self, interface: &'static Interface<'static>) -> Self { fn add_mcp_routes(self, interface: &'static Interface<'static>, mcp: bool) -> Self {
if !mcp {
return self;
}
let config = StreamableHttpServerConfig { let config = StreamableHttpServerConfig {
// stateful_mode: false, // stateful_mode: false, // breaks Claude
..Default::default() ..Default::default()
}; };
@@ -27,6 +32,8 @@ impl MCPRoutes for Router<AppState> {
config, config,
); );
info!("Setting MCP...");
self.nest_service("/mcp", service) self.nest_service("/mcp", service)
} }
} }