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

104
Cargo.lock generated
View File

@@ -585,11 +585,11 @@ dependencies = [
"brk_computer",
"brk_core",
"brk_indexer",
"brk_rmcp",
"brk_vec",
"color-eyre",
"derive_deref",
"rmcp",
"schemars",
"schemars 1.0.0-rc.2",
"serde",
"serde_json",
"serde_with",
@@ -621,6 +621,48 @@ dependencies = [
"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]]
name = "brk_rolldown"
version = "0.0.1"
@@ -941,6 +983,7 @@ dependencies = [
"brk_interface",
"brk_logger",
"brk_parser",
"brk_rmcp",
"brk_vec",
"clap",
"clap_derive",
@@ -948,7 +991,6 @@ dependencies = [
"jiff",
"log",
"minreq",
"rmcp",
"serde",
"tokio",
"tower-http",
@@ -3402,44 +3444,6 @@ dependencies = [
"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]]
name = "rolldown-ariadne"
version = "0.5.2"
@@ -3539,6 +3543,18 @@ name = "schemars"
version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
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 = [
"dyn-clone",
"ref-cast",
@@ -3549,9 +3565,9 @@ dependencies = [
[[package]]
name = "schemars_derive"
version = "0.9.0"
version = "1.0.0-rc.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5016d94c77c6d32f0b8e08b781f7dc8a90c2007d4e77472cc2807bc10a8438fe"
checksum = "3ce51e6bea03670e88edba2d724eed817b97f4ab017a1001bae5da325ce93fe7"
dependencies = [
"proc-macro2",
"quote",
@@ -3713,7 +3729,7 @@ dependencies = [
"hex",
"indexmap 1.9.3",
"indexmap 2.9.0",
"schemars",
"schemars 0.9.0",
"serde",
"serde_derive",
"serde_json",

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_fetcher = { version = "0.0.66", path = "crates/brk_fetcher" }
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_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_state = { version = "0.0.66", path = "crates/brk_state" }
brk_store = { version = "0.0.66", path = "crates/brk_store" }
@@ -46,10 +47,7 @@ jiff = "0.2.15"
log = { version = "0.4.27" }
minreq = { version = "2.13.4", features = ["https", "serde_json"] }
rayon = "1.10.0"
rmcp = { path = "../rust-sdk/crates/rmcp", features = ["transport-streamable-http-server", "transport-worker"] }
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"
schemars = "1.0.0-rc.2"
serde = { version = "1.0.219" }
serde_bytes = "0.11.17"
serde_derive = "1.0.219"

View File

@@ -87,7 +87,12 @@ pub struct Config {
#[arg(long, value_name = "SECONDS")]
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")]
#[arg(long, value_name = "BOOL")]
watch: Option<bool>,
@@ -171,6 +176,10 @@ impl Config {
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() {
config_saved.watch = Some(watch);
}
@@ -356,6 +365,10 @@ impl Config {
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 {
self.watch.is_some_and(|b| b)
}

View File

@@ -57,8 +57,9 @@ pub fn run() -> color_eyre::Result<()> {
let server = Server::new(served_indexer, served_computer, config.website())?;
let watch = config.watch();
let mcp = config.mcp();
let opt = Some(tokio::spawn(async move {
server.serve(watch).await.unwrap();
server.serve(watch, mcp).await.unwrap();
}));
sleep(Duration::from_secs(1));

View File

@@ -14,7 +14,7 @@ brk_indexer = { workspace = true }
brk_vec = { workspace = true }
color-eyre = { workspace = true }
derive_deref = { workspace = true }
rmcp = { workspace = true }
brk_rmcp = { workspace = true }
schemars = { workspace = true }
serde = { workspace = true }
serde_json = { workspace = true }

View File

@@ -1,5 +1,5 @@
use brk_rmcp::schemars::JsonSchema;
use color_eyre::eyre::eyre;
use rmcp::schemars::JsonSchema;
use serde::Deserialize;
#[allow(clippy::upper_case_acronyms)]

View File

@@ -165,6 +165,7 @@ impl<'de> Deserialize<'de> for Index {
{
let str = String::deserialize(deserializer)?;
if let Ok(index) = Index::try_from(str.as_str()) {
// dbg!(index);
Ok(index)
} else {
Err(Error::custom("Bad index"))

View File

@@ -15,6 +15,7 @@ mod format;
mod index;
mod maybe_ids;
mod output;
mod pagination;
mod params;
mod table;
mod vecs;
@@ -22,7 +23,8 @@ mod vecs;
pub use format::Format;
pub use index::Index;
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;
use vecs::Vecs;
@@ -192,21 +194,15 @@ impl<'a> Interface<'a> {
&self.vecs.accepted_indexes
}
pub fn get_vecids(&self, pagination: Pagination) -> &[&str] {
pub fn get_vecids(&self, pagination: PaginationParam) -> &[&str] {
self.vecs.ids(pagination)
}
pub fn get_indexes_to_vecids(
&self,
pagination: Pagination,
) -> BTreeMap<&'static str, Vec<&str>> {
self.vecs.indexes_to_ids(pagination)
pub fn get_index_to_vecids(&self, paginated_index: PaginatedIndexParam) -> Vec<&str> {
self.vecs.index_to_ids(paginated_index)
}
pub fn get_vecids_to_indexes(
&self,
pagination: Pagination,
) -> BTreeMap<&str, Vec<&'static str>> {
self.vecs.ids_to_indexes(pagination)
pub fn get_vecid_to_indexes(&self, id: String) -> Option<&Vec<&'static str>> {
self.vecs.id_to_indexes(id)
}
}

View File

@@ -22,9 +22,17 @@ impl<'de> Deserialize<'de> for MaybeIds {
where
D: serde::Deserializer<'de>,
{
let str = String::deserialize(deserializer)?;
Ok(MaybeIds(
str.split(",").map(|s| s.to_string()).collect::<Vec<_>>(),
))
let maybe_ids = match serde_json::Value::deserialize(deserializer)? {
serde_json::Value::String(str) => {
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))
}
}

View File

@@ -6,6 +6,7 @@ use tabled::Tabled as TabledTabled;
use crate::Format;
#[derive(Debug, Serialize)]
#[serde(untagged)]
pub enum Output {
Json(Value),
CSV(String),

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,
}

View File

@@ -1,6 +1,6 @@
use std::ops::Deref;
use rmcp::schemars::{self, JsonSchema};
use brk_rmcp::schemars::{self, JsonSchema};
use serde::{Deserialize, Deserializer};
use crate::{Format, Index, maybe_ids::MaybeIds};
@@ -52,6 +52,7 @@ pub struct ParamsOpt {
count: Option<usize>,
/// Format of the output
#[serde(default)]
format: Option<Format>,
}
@@ -85,7 +86,7 @@ impl ParamsOpt {
if let Some(c) = self.count {
let c = c as i64;
if let Some(f) = self.from {
if f.is_positive() || f.abs() > c {
if f >= 0 || f.abs() > c {
return Some(f + c);
}
} 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>
where
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)
}
}

View File

@@ -5,7 +5,7 @@ use brk_indexer::Indexer;
use brk_vec::AnyCollectableVec;
use derive_deref::{Deref, DerefMut};
use crate::params::Pagination;
use crate::pagination::{PaginatedIndexParam, PaginationParam};
use super::index::Index;
@@ -19,8 +19,8 @@ pub struct Vecs<'a> {
pub index_count: usize,
pub id_count: usize,
pub vec_count: usize,
serialized_id_to_indexes: BTreeMap<&'a str, Vec<&'static str>>,
serialized_indexes_to_ids: BTreeMap<&'static str, Vec<&'a str>>,
id_to_indexes: BTreeMap<&'a str, Vec<&'static str>>,
indexes_to_ids: BTreeMap<Index, Vec<&'a str>>,
}
impl<'a> Vecs<'a> {
@@ -40,17 +40,22 @@ impl<'a> Vecs<'a> {
.for_each(|vec| this.insert(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());
if len_cmp == std::cmp::Ordering::Equal {
a.cmp(b)
} else {
len_cmp
}
});
let sort_ids = |ids: &mut Vec<&str>| {
ids.sort_unstable_by(|a, b| {
let len_cmp = a.len().cmp(&b.len());
if len_cmp == std::cmp::Ordering::Equal {
a.cmp(b)
} else {
len_cmp
}
})
};
sort_ids(&mut 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.vec_count = this
.index_to_id_to_vec
@@ -67,7 +72,7 @@ impl<'a> Vecs<'a> {
.keys()
.map(|i| (i.serialize_long(), i.possible_values()))
.collect::<BTreeMap<_, _>>();
this.serialized_id_to_indexes = this
this.id_to_indexes = this
.id_to_index_to_vec
.iter()
.map(|(id, index_to_vec)| {
@@ -80,16 +85,14 @@ impl<'a> Vecs<'a> {
)
})
.collect();
this.serialized_indexes_to_ids = this
this.indexes_to_ids = this
.index_to_id_to_vec
.iter()
.map(|(index, id_to_vec)| {
(
index.serialize_long(),
id_to_vec.keys().cloned().collect::<Vec<_>>(),
)
})
.map(|(index, id_to_vec)| (*index, id_to_vec.keys().cloned().collect::<Vec<_>>()))
.collect();
this.indexes_to_ids
.values_mut()
.for_each(|ids| sort_ids(ids));
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 start = pagination.start(len);
let end = pagination.end(len);
&self.ids[start..end]
}
pub fn ids_to_indexes(&self, pagination: Pagination) -> BTreeMap<&'_ str, Vec<&'static str>> {
let len = self.serialized_id_to_indexes.len();
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 id_to_indexes(&self, id: String) -> Option<&Vec<&'static str>> {
self.id_to_indexes.get(id.as_str())
}
pub fn indexes_to_ids(&self, pagination: Pagination) -> BTreeMap<&'static str, Vec<&'a str>> {
let len = self.serialized_indexes_to_ids.len();
pub fn index_to_ids(
&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 end = pagination.end(len);
self.serialized_indexes_to_ids
.iter()
.skip(start)
.take(end)
.map(|(index, ids)| (*index, ids.clone()))
.collect()
vec.iter().skip(start).take(end).cloned().collect()
}
}

View File

@@ -25,10 +25,9 @@ pub fn init(path: Option<&Path>) {
.unwrap()
});
Builder::from_env(
Env::default()
.default_filter_or("info,fjall=off,lsm_tree=off,rolldown=off,brk_rolldown=off"),
)
Builder::from_env(Env::default().default_filter_or(
"info,fjall=off,lsm_tree=off,rolldown=off,brk_rolldown=off,rmcp=off,brk_rmcp=off,tracing=off",
))
.format(move |buf, record| {
let date_time = Timestamp::now()
.to_zoned(tz::TimeZone::system())
@@ -83,6 +82,7 @@ fn write(
args: impl Display,
) -> Result<(), std::io::Error> {
writeln!(buf, "{} {} {} {}", date_time, dash, level, args)
// Don't remove, used to know the target of unwanted logs
// writeln!(
// buf,
// "{} {} {} {} {}",

View File

@@ -26,7 +26,7 @@ color-eyre = { workspace = true }
jiff = { workspace = true }
log = { workspace = true }
minreq = { workspace = true }
rmcp = { workspace = true }
brk_rmcp = { workspace = true }
serde = { workspace = true }
tokio = { workspace = true }
tower-http = { version = "0.6.6", features = ["compression-full", "trace"] }

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)
Count of all possible indexes
Get the count of all existing indexes.
#### [`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)
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 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 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)
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)

View File

@@ -48,7 +48,7 @@ pub fn main() -> color_eyre::Result<()> {
let server = Server::new(served_indexer, served_computer, Website::Default)?;
let server = tokio::spawn(async move {
server.serve(true).await.unwrap();
server.serve(true, true).await.unwrap();
});
if process {

View File

@@ -5,7 +5,7 @@ use axum::{
response::{IntoResponse, Redirect, Response},
routing::get,
};
use brk_interface::{Index, Pagination, Params, ParamsOpt};
use brk_interface::{IdParam, Index, PaginatedIndexParam, PaginationParam, Params, ParamsOpt};
use super::AppState;
@@ -18,7 +18,7 @@ pub trait ApiRoutes {
fn add_api_routes(self) -> Self;
}
const TO_SEPARATOR: &str = "-to-";
const TO_SEPARATOR: &str = "_to_";
impl ApiRoutes for Router<AppState> {
fn add_api_routes(self) -> Self {
@@ -56,29 +56,29 @@ impl ApiRoutes for Router<AppState> {
"/api/vecs/ids",
get(
async |State(app_state): State<AppState>,
Query(pagination): Query<Pagination>|
Query(pagination): Query<PaginationParam>|
-> Response {
Json(app_state.interface.get_vecids(pagination)).into_response()
},
),
)
.route(
"/api/vecs/indexes-to-ids",
"/api/vecs/index-to-ids",
get(
async |State(app_state): State<AppState>,
Query(pagination): Query<Pagination>|
Query(paginated_index): Query<PaginatedIndexParam>|
-> 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(
"/api/vecs/ids-to-indexes",
"/api/vecs/id-to-indexes",
get(
async |State(app_state): State<AppState>,
Query(pagination): Query<Pagination>|
Query(param): Query<IdParam>|
-> 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>,
state: State<AppState>|
-> Response {
let variant = variant.replace("_", "-");
let variant = variant.replace("-", "_");
let mut split = variant.split(TO_SEPARATOR);
let params = Params::from((
(
Index::try_from(split.next().unwrap()).unwrap(),
split.collect::<Vec<_>>().join(TO_SEPARATOR),
),
params_opt,
));
interface::handler(headers, Query(params), state).await
if let Ok(index) = Index::try_from(split.next().unwrap()) {
let params = Params::from((
(index, split.collect::<Vec<_>>().join(TO_SEPARATOR)),
params_opt,
));
interface::handler(headers, Query(params), state).await
} else {
"Bad variant".into_response()
}
},
),
)

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;
if let Some(websites_path) = state.websites_path.clone() {
@@ -162,7 +162,7 @@ impl Server {
let router = Router::new()
.add_api_routes()
.add_website_routes(state.website)
.add_mcp_routes(state.interface)
.add_mcp_routes(state.interface, mcp)
.route("/version", get(Json(VERSION)))
.with_state(state)
.layer(compression_layer)

View File

@@ -1,5 +1,5 @@
use brk_interface::{Interface, Pagination, Params};
use rmcp::{
use brk_interface::{IdParam, Interface, PaginatedIndexParam, PaginationParam, Params};
use brk_rmcp::{
Error as McpError, RoleServer, ServerHandler,
model::{
CallToolResult, Content, Implementation, InitializeRequestParam, InitializeResult,
@@ -8,6 +8,7 @@ use rmcp::{
service::RequestContext,
tool,
};
use log::info;
#[derive(Clone)]
pub struct API {
@@ -23,88 +24,111 @@ impl API {
}
#[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> {
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
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
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
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
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 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(
&self,
#[tool(aggr)] pagination: Pagination,
#[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 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,
#[tool(aggr)] pagination: Pagination,
#[tool(aggr)] paginated_index: PaginatedIndexParam,
) -> Result<CallToolResult, McpError> {
info!("mcp: get_index_to_vecids");
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 = "
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,
#[tool(aggr)] pagination: Pagination,
#[tool(aggr)] param: IdParam,
) -> Result<CallToolResult, McpError> {
info!("mcp: get_vecid_to_indexes");
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> {
info!("mcp: get_vecs");
Ok(CallToolResult::success(vec![
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
")]
async fn get_version(&self) -> Result<CallToolResult, McpError> {
info!("mcp: get_version");
Ok(CallToolResult::success(vec![Content::text(format!(
"v{VERSION}"
))]))
@@ -124,14 +149,22 @@ Get the running version of the Bitcoin Research Kit
impl ServerHandler for API {
fn get_info(&self) -> ServerInfo {
ServerInfo {
protocol_version: ProtocolVersion::V_2025_03_26,
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 (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).
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(),
),

View File

@@ -1,23 +1,28 @@
use axum::Router;
use brk_interface::Interface;
use rmcp::transport::{
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>) -> Self;
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>) -> Self {
fn add_mcp_routes(self, interface: &'static Interface<'static>, mcp: bool) -> Self {
if !mcp {
return self;
}
let config = StreamableHttpServerConfig {
// stateful_mode: false,
// stateful_mode: false, // breaks Claude
..Default::default()
};
@@ -27,6 +32,8 @@ impl MCPRoutes for Router<AppState> {
config,
);
info!("Setting MCP...");
self.nest_service("/mcp", service)
}
}