diff --git a/Cargo.lock b/Cargo.lock index ff2a580..474bd31 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1171,6 +1171,12 @@ dependencies = [ "zeroize", ] +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + [[package]] name = "levenshtein_automata" version = "0.2.1" @@ -1267,10 +1273,11 @@ dependencies = [ "rust-mcp-sdk", "serde", "serde_json", - "strsim", "tantivy", "tokio", "tokio-util", + "tracing", + "tracing-subscriber", "uuid", ] @@ -1358,6 +1365,15 @@ dependencies = [ "minimal-lexical", ] +[[package]] +name = "nu-ansi-term" +version = "0.50.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" +dependencies = [ + "windows-sys 0.61.2", +] + [[package]] name = "num-bigint" version = "0.4.8" @@ -2026,6 +2042,15 @@ dependencies = [ "digest", ] +[[package]] +name = "sharded-slab" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" +dependencies = [ + "lazy_static", +] + [[package]] name = "shlex" version = "2.0.1" @@ -2335,6 +2360,15 @@ dependencies = [ "syn 3.0.5", ] +[[package]] +name = "thread_local" +version = "1.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ad99c4c6d32803332c548b1af0540b357b3f5fc0be8f6c6bfe8b2e6ae784070" +dependencies = [ + "cfg-if", +] + [[package]] name = "time" version = "0.3.55" @@ -2531,6 +2565,32 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" dependencies = [ "once_cell", + "valuable", +] + +[[package]] +name = "tracing-log" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" +dependencies = [ + "log", + "once_cell", + "tracing-core", +] + +[[package]] +name = "tracing-subscriber" +version = "0.3.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb7f578e5945fb242538965c2d0b04418d38ec25c79d160cd279bf0731c8d319" +dependencies = [ + "nu-ansi-term", + "sharded-slab", + "smallvec", + "thread_local", + "tracing-core", + "tracing-log", ] [[package]] @@ -2642,6 +2702,12 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "valuable" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" + [[package]] name = "version_check" version = "0.9.5" diff --git a/check_output.txt b/check_output.txt new file mode 100644 index 0000000..5636012 --- /dev/null +++ b/check_output.txt @@ -0,0 +1 @@ + Finished `dev` profile [unoptimized + debuginfo] target(s) in 14.64s diff --git a/clippy_output.txt b/clippy_output.txt new file mode 100644 index 0000000..098ea08 --- /dev/null +++ b/clippy_output.txt @@ -0,0 +1,312 @@ + Blocking waiting for file lock on package cache + Blocking waiting for file lock on package cache + Compiling proc-macro2 v1.0.107 + Compiling quote v1.0.47 + Compiling unicode-ident v1.0.24 + Compiling getrandom v0.4.3 + Compiling serde_core v1.0.229 + Checking windows-link v0.2.1 + Compiling cfg-if v1.0.4 + Compiling shlex v2.0.1 + Compiling find-msvc-tools v0.1.12 + Checking smallvec v1.16.0 + Checking stable_deref_trait v1.2.1 + Checking windows-sys v0.61.2 + Checking pin-project-lite v0.2.17 + Checking futures-core v0.3.34 + Checking memchr v2.8.3 + Checking itoa v1.0.18 + Compiling serde v1.0.229 + Checking bytes v1.12.1 + Checking once_cell v1.21.4 + Checking futures-sink v0.3.34 + Compiling parking_lot_core v0.9.12 + Checking scopeguard v1.2.0 + Checking slab v0.4.12 + Checking mio v1.2.3 + Checking socket2 v0.6.5 + Checking log v0.4.34 + Checking futures-channel v0.3.34 + Compiling version_check v0.9.5 + Compiling jobserver v0.1.35 + Checking lock_api v0.4.14 + Compiling zmij v1.0.23 + Checking futures-task v0.3.34 + Checking futures-io v0.3.34 + Compiling num-conv v0.2.2 + Compiling time-core v0.1.9 + Compiling cc v1.4.5 + Compiling autocfg v1.5.1 + Checking deranged v0.5.8 + Checking http v1.5.0 + Checking tracing-core v0.1.36 + Compiling syn v3.0.5 + Compiling syn v2.0.119 + Checking percent-encoding v2.3.2 + Checking litemap v0.8.3 + Compiling time-macros v0.2.32 + Checking writeable v0.6.4 + Checking powerfmt v0.2.0 + Checking equivalent v1.0.2 + Checking parking_lot v0.12.5 + Checking utf8_iter v1.0.4 + Compiling icu_normalizer_data v2.3.0 + Compiling icu_properties_data v2.3.0 + Checking fnv v1.0.7 + Compiling serde_json v1.0.151 + Checking http-body v1.1.0 + Checking hashbrown v0.17.1 + Compiling httparse v1.10.1 + Checking base64 v0.22.1 + Compiling synstructure v0.13.2 + Checking tower-service v0.3.3 + Compiling ring v0.17.14 + Compiling num-traits v0.2.19 + Checking indexmap v2.14.2 + Checking time v0.3.55 + Checking try-lock v0.2.5 + Compiling serde_derive v1.0.229 + Compiling zerovec-derive v0.11.6 + Compiling tokio-macros v2.7.2 + Compiling zerofrom-derive v0.1.7 + Compiling yoke-derive v0.8.2 + Compiling displaydoc v0.2.7 + Compiling futures-macro v0.3.34 + Compiling tracing-attributes v0.1.31 + Compiling zeroize_derive v1.5.0 + Checking tokio v1.53.1 + Compiling libc v0.2.189 + Checking atomic-waker v1.1.2 + Checking futures-util v0.3.34 + Checking subtle v2.6.1 + Checking zerofrom v0.1.8 + Checking want v0.3.1 + Checking zeroize v1.9.0 + Checking tracing v0.1.44 + Checking form_urlencoded v1.2.2 + Checking yoke v0.8.3 + Checking getrandom v0.2.17 + Checking httpdate v1.0.3 + Checking untrusted v0.9.0 + Checking rustls-pki-types v1.15.1 + Compiling generic-array v0.14.7 + Checking sync_wrapper v1.0.2 + Checking tower-layer v0.3.3 + Checking tokio-util v0.7.19 + Checking zerovec v0.11.8 + Checking zerotrie v0.2.5 + Compiling rustls v0.23.44 + Compiling pkg-config v0.3.34 + Checking ipnet v2.12.2 + Compiling async-trait v0.1.92 + Compiling cmake v0.1.58 + Compiling cookie v0.18.2 + Checking h2 v0.4.19 + Checking tinystr v0.8.4 + Checking potential_utf v0.1.6 + Compiling dunce v1.0.5 + Checking mime v0.3.17 + Compiling zstd-sys v2.1.0+zstd.1.5.7 + Compiling unicase v2.9.0 + Checking icu_locale_core v2.3.0 + Compiling thiserror v2.0.20 + Checking icu_collections v2.3.0 + Compiling fs_extra v1.3.0 + Compiling crossbeam-utils v0.8.23 + Checking typenum v1.20.1 + Compiling mime_guess v2.0.5 + Checking hyper v1.11.1 + Checking tower v0.5.3 + Checking rustls-webpki v0.103.15 + Checking icu_provider v2.3.1 + Compiling thiserror-impl v2.0.20 + Compiling aws-lc-sys v0.45.0 + Checking http-body-util v0.1.5 + Checking icu_normalizer v2.3.0 + Checking icu_properties v2.3.0 + Checking hyper-util v0.1.20 + Compiling litrs v1.0.0 + Checking ryu v1.0.23 + Checking psl-types v2.0.11 + Checking either v1.18.0 + Compiling crunchy v0.2.4 + Checking idna_adapter v1.2.2 + Checking serde_urlencoded v0.7.1 + Compiling document-features v0.2.12 + Checking idna v1.1.0 + Checking webpki-roots v1.0.9 + Compiling ident_case v1.0.1 + Checking tokio-rustls v0.26.5 + Checking byteorder v1.5.0 + Checking url v2.5.8 + Checking publicsuffix v2.3.0 + Checking bitflags v2.13.1 + Compiling crossbeam-epoch v0.9.21 + Compiling windows_x86_64_msvc v0.52.6 + Compiling zstd-safe v7.3.0 + Checking regex-syntax v0.8.11 + Compiling strsim v0.11.1 + Checking hyper-rustls v0.27.9 + Checking cookie_store v0.22.1 + Checking tower-http v0.6.11 + Compiling darling_core v0.24.1 + Checking block-buffer v0.10.4 + Checking crypto-common v0.1.7 + Checking num-integer v0.1.47 + Checking ownedbytes v0.9.0 + Compiling crossbeam-deque v0.8.8 + Compiling typeid v1.0.3 + Checking once_cell_polyfill v1.70.2 + Compiling aws-lc-rs v1.18.1 + Checking utf8parse v0.2.2 + Compiling prettyplease v0.3.0 + Checking anstyle v1.0.14 + Checking tantivy-common v0.11.0 + Checking anstyle-parse v1.0.0 + Compiling darling_macro v0.24.1 + Checking anstyle-wincon v3.0.11 + Checking num-bigint v0.4.8 + Checking digest v0.10.7 + Checking reqwest v0.12.28 + Checking bitpacking v0.9.3 + Checking futures-executor v0.3.34 + Compiling fs-err v3.3.1 + Checking anstyle-query v1.1.5 + Compiling rayon-core v1.13.0 + Checking colorchoice v1.0.5 + Checking is_terminal_polyfill v1.70.2 + Checking utf8-ranges v1.0.5 + Compiling rustversion v1.0.23 + Checking untrusted v0.7.1 + Compiling erased-serde v0.4.10 + Checking futures v0.3.34 + Checking tantivy-bitpacker v0.10.0 + Checking anstream v1.0.0 + Checking zstd v0.13.3 + Checking tantivy-fst v0.5.0 + Checking simple_asn1 v0.6.4 + Checking windows-targets v0.52.6 + Compiling darling v0.24.1 + Checking uuid v1.26.0 + Checking itertools v0.14.0 + Checking tokio-stream v0.1.19 + Checking rust-mcp-schema v2.0.0 + Checking pem v3.0.6 + Compiling crc32fast v1.5.1 + Checking minimal-lexical v0.2.1 + Checking signature v2.2.0 + Checking murmurhash32 v0.3.1 + Checking clap_lex v1.1.0 + Compiling heck v0.5.0 + Checking cpufeatures v0.2.17 + Checking allocator-api2 v0.2.21 + Checking foldhash v0.2.0 + Compiling typetag v0.2.23 + Compiling winapi v0.3.9 + Checking nom v7.1.3 + Checking tantivy-stacker v0.7.0 + Checking sha2 v0.10.9 + Compiling clap_derive v4.6.4 + Checking tantivy-sstable v0.7.0 + Checking hashbrown v0.16.1 + Checking clap_builder v4.6.6 + Compiling rust-mcp-macros v2.0.0 + Compiling bon-macros v3.10.1 + Checking rust-mcp-transport v2.0.0 + Checking windows-sys v0.59.0 + Checking hmac v0.12.1 + Checking regex-automata v0.4.18 + Checking axum-core v0.5.6 + Checking ordered-float v5.5.0 + Compiling typetag-impl v0.2.23 + Checking serde_path_to_error v0.1.20 + Checking fastrand v2.5.0 + Checking inventory v0.3.24 + Checking downcast-rs v2.0.2 + Checking matchit v0.8.4 + Checking fastdivide v0.4.2 + Checking option-ext v0.2.0 + Checking base64 v0.23.1 + Checking fs4 v0.13.1 + Checking arc-swap v1.9.2 + Checking tantivy-columnar v0.7.0 + Checking dirs-sys v0.5.0 + Checking tempfile v3.27.0 + Checking clap v4.6.6 + Checking regex v1.13.1 + Checking tantivy-query-grammar v0.26.0 + Checking axum v0.8.9 + Checking rayon v1.12.0 + Checking axum-server v0.8.0 + Checking lru v0.16.4 + Checking crossbeam-channel v0.5.17 + Checking rust-stemmers v1.2.0 + Checking sketches-ddsketch v0.4.1 + Checking tantivy-tokenizer-api v0.7.0 + Checking measure_time v0.9.0 + Checking aho-corasick v1.1.5 + Checking htmlescape v0.3.1 + Checking lz4_flex v0.13.1 + Checking bon v3.10.1 + Checking memmap2 v0.9.11 + Checking datasketches v0.2.0 + Checking levenshtein_automata v0.2.1 + Checking rustc-hash v2.1.3 + Checking oneshot v0.1.13 + Checking census v0.4.2 + Checking dirs v6.0.0 + Checking glob v0.3.4 + Checking mcp-memory-stub v0.1.0 (C:\Users\reazul.ashraf\workspace\rust\mcp-memory\stub) + Checking tantivy v0.26.1 +warning: this can be `std::io::Error::other(_)` + --> stub\src\main.rs:76:72 + | +76 | let stream = resp.bytes_stream().map(|res| res.map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, e))); + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + | + = help: for further information visit https://rust-lang.github.io/rust-clippy/rust-1.98.0/index.html#io_other_error + = note: `#[warn(clippy::io_other_error)]` on by default +help: use `std::io::Error::other` + | +76 - let stream = resp.bytes_stream().map(|res| res.map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, e))); +76 + let stream = resp.bytes_stream().map(|res| res.map_err(|e| std::io::Error::other(e))); + | + +warning: stripping a prefix manually + --> stub\src\main.rs:93:40 + | +93 | println!("{}", &trimmed[6..]); + | ^^^^^^^^^^^^^ + | +note: the prefix was tested here + --> stub\src\main.rs:91:24 + | +91 | } else if trimmed.starts_with("data: ") { + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + = help: for further information visit https://rust-lang.github.io/rust-clippy/rust-1.98.0/index.html#manual_strip + = note: `#[warn(clippy::manual_strip)]` on by default +help: try using the `strip_prefix` method + | +91 ~ } else if let Some() = trimmed.strip_prefix("data: ") { +92 | if is_message { +93 ~ println!("{}", ); +94 | is_message = false; +95 | } else if is_endpoint { +96 ~ let ep = ; + | + +warning: redundant pattern matching + --> stub\src\main.rs:117:16 + | +117 | if let Err(_) = std::net::TcpStream::connect(cli.target.replace("http://", "").replace("https://", "")) { + | ^^^^^^ + | + = help: for further information visit https://rust-lang.github.io/rust-clippy/rust-1.98.0/index.html#redundant_pattern_matching + = note: `#[warn(clippy::redundant_pattern_matching)]` on by default +help: consider using `is_err()` + | +117 - if let Err(_) = std::net::TcpStream::connect(cli.target.replace("http://", "").replace("https://", "")) { +117 + if std::net::TcpStream::connect(cli.target.replace("http://", "").replace("https://", "")).is_err() { + | + +warning: `mcp-memory-stub` (bin "mcp-memory-stub") generated 3 warnings (run `cargo clippy --fix --bin "mcp-memory-stub" -p mcp-memory-stub -- ` to apply 2 suggestions) diff --git a/mcp_client.py b/mcp_client.py new file mode 100644 index 0000000..16cc24b --- /dev/null +++ b/mcp_client.py @@ -0,0 +1,59 @@ +import urllib.request +import json +import threading +import sys +import time + +sse_url = "http://localhost:3000/sse" + +def call_mcp(method, params): + req = urllib.request.Request(sse_url) + with urllib.request.urlopen(req) as response: + endpoint = None + # read the SSE stream + for line_bytes in response: + line = line_bytes.decode('utf-8').strip() + if line.startswith("data: /messages"): + endpoint = "http://localhost:3000" + line.split("data: ")[1].strip() + break + + if not endpoint: + print("Failed to get endpoint") + sys.exit(1) + + print(f"Got endpoint: {endpoint}") + + payload = { + "jsonrpc": "2.0", + "id": 1, + "method": "tools/call", + "params": { + "name": method, + "arguments": params + } + } + + post_req = urllib.request.Request(endpoint, data=json.dumps(payload).encode('utf-8'), headers={'Content-Type': 'application/json'}) + with urllib.request.urlopen(post_req) as post_resp: + print(f"POST status: {post_resp.status}") + + # continue reading SSE for response + for line_bytes in response: + line = line_bytes.decode('utf-8').strip() + if line.startswith("data: "): + data = line[6:] + if data == "ping": continue + try: + msg = json.loads(data) + if msg.get("id") == 1: + print("RESULT:") + print(json.dumps(msg, indent=2)) + return + except: + pass + +import sys +if __name__ == "__main__": + method = sys.argv[1] + params = json.loads(sys.argv[2]) + call_mcp(method, params) diff --git a/server/Cargo.toml b/server/Cargo.toml index 722c656..2c0e38a 100644 --- a/server/Cargo.toml +++ b/server/Cargo.toml @@ -15,8 +15,9 @@ rust-mcp-axum = "2.0.0" rust-mcp-sdk = "2.0.0" serde = { version = "1.0.229", features = ["derive"] } serde_json = "1.0.151" -strsim = "0.11.1" tantivy = "0.26.1" tokio = { version = "1.53.1", features = ["full"] } tokio-util = { version = "0.7.19", features = ["io"] } +tracing = "0.1.44" +tracing-subscriber = "0.3.23" uuid = { version = "1.26.0", features = ["v4"] } diff --git a/server/src/dashboard.html b/server/src/dashboard.html new file mode 100644 index 0000000..54a580d --- /dev/null +++ b/server/src/dashboard.html @@ -0,0 +1,73 @@ + + + + Brain Monitor + + + +
+

🧠 Antigravity Brain Monitor

+
System Online
+
+ +
+
+
Entities
+
-
+
+
+
Relations
+
-
+
+
+
Active Tasks
+
-
+
+
+
Snippets
+
-
+
+
+
Tech Debt
+
-
+
+
+
ADRs
+
-
+
+
+ + + + diff --git a/server/src/handlers.rs b/server/src/handlers.rs index e896b6a..5519c06 100644 --- a/server/src/handlers.rs +++ b/server/src/handlers.rs @@ -120,6 +120,9 @@ impl ServerHandler for MemoryHandler { } { let entity: Entity = e; if !entity.name.is_empty() { + if let Ok(idx) = self.state.search_index.read() { + let _ = idx.index_entity(&entity); + } g.entities.insert(entity.name.clone(), entity); } } @@ -267,29 +270,19 @@ impl ServerHandler for MemoryHandler { } "search_nodes" => { let req: SearchNodesTool = parse_args(args)?; - let query = req.query.to_lowercase(); - let tokens: Vec<&str> = query.split_whitespace().collect(); + let matches = if let Ok(idx) = self.state.search_index.read() { + idx.search(&req.query, req.namespace.as_deref()).unwrap_or_default() + } else { + vec![] + }; + let mut result = KnowledgeGraph::default(); - for (name, e) in self.state.get_full_graph().entities { - if let Some(ns) = &req.namespace - && e.namespace != *ns - { - continue; - } - let name_low = name.to_lowercase(); - let type_low = e.entity_type.to_lowercase(); - let full_txt = format!( - "{} {} {}", - name_low, - type_low, - e.observations.join(" ").to_lowercase() - ); - let matched = full_txt.contains(&query) - || (!tokens.is_empty() && tokens.iter().all(|&t| full_txt.contains(t))) - || strsim::jaro_winkler(&name_low, &query) > 0.85 - || strsim::jaro_winkler(&type_low, &query) > 0.85; - if matched { - result.entities.insert(name, e); + let full = self.state.get_full_graph(); + for (id, doc_type) in matches { + if doc_type == "entity" { + if let Some(e) = full.entities.get(&id) { + result.entities.insert(id, e.clone()); + } } } let data = serde_json::to_string(&result).unwrap_or_default(); @@ -456,16 +449,20 @@ impl ServerHandler for MemoryHandler { .unwrap() .as_secs(); let id = uuid::Uuid::new_v4().to_string(); + let task = Task { + id: id.clone(), + title: req.title, + status: "pending".to_string(), + description: req.description, + created_at: now, + updated_at: now, + git_branch: req.git_branch, + }; + if let Ok(idx) = self.state.search_index.read() { + let _ = idx.index_task(&task); + } self.state.tasks.modify(|tasks| { - tasks.push(Task { - id: id.clone(), - title: req.title, - status: "pending".to_string(), - description: req.description, - created_at: now, - updated_at: now, - git_branch: req.git_branch, - }); + tasks.push(task); }); Ok(ServerResult::from(CallToolResult::text_content(vec![ format!("Task added with ID: {}", id).into(), @@ -1098,47 +1095,42 @@ impl ServerHandler for MemoryHandler { } "omni_search" => { let req: OmniSearchTool = parse_args(args)?; + let matches = if let Ok(idx) = self.state.search_index.read() { + idx.search(&req.query, req.namespace.as_deref()).unwrap_or_default() + } else { + vec![] + }; + + let mut kg = KnowledgeGraph::default(); + let mut tasks = Vec::new(); + let mut snippets = Vec::new(); + let mut adrs = Vec::new(); + + let full = self.state.get_full_graph(); + for (id, doc_type) in &matches { + if doc_type == "entity" { + if let Some(e) = full.entities.get(id) { + kg.entities.insert(id.clone(), e.clone()); + } + } + } + for t in self.state.tasks.read() { + if matches.iter().any(|(id, typ)| id == &t.id && typ == "task") { + tasks.push(t); + } + } + for s in self.state.snippets.read() { + if matches.iter().any(|(id, typ)| id == &s.name && typ == "snippet") { + snippets.push(s); + } + } + for a in self.state.adrs.read() { + if matches.iter().any(|(id, typ)| id == &a.id && typ == "adr") { + adrs.push(a); + } + } + let q = req.query.to_lowercase(); - - let mut kg = self.state.get_full_graph(); - kg.entities.retain(|name, e| { - (req.namespace.is_none() || e.namespace == *req.namespace.as_ref().unwrap()) - && (name.to_lowercase().contains(&q) - || e.entity_type.to_lowercase().contains(&q) - || e.observations.join(" ").to_lowercase().contains(&q)) - }); - - let tasks: Vec<_> = self - .state - .tasks - .read() - .into_iter() - .filter(|t| { - t.title.to_lowercase().contains(&q) - || t.description.to_lowercase().contains(&q) - }) - .collect(); - let snippets: Vec<_> = self - .state - .snippets - .read() - .into_iter() - .filter(|s| { - s.name.to_lowercase().contains(&q) - || s.description.to_lowercase().contains(&q) - }) - .collect(); - let adrs: Vec<_> = self - .state - .adrs - .read() - .into_iter() - .filter(|a| { - a.title.to_lowercase().contains(&q) - || a.context.to_lowercase().contains(&q) - || a.decision.to_lowercase().contains(&q) - }) - .collect(); let tech_debts: Vec<_> = self .state .tech_debts diff --git a/server/src/main.rs b/server/src/main.rs index e262591..49df924 100644 --- a/server/src/main.rs +++ b/server/src/main.rs @@ -3,6 +3,7 @@ mod models; mod state; mod store; mod tools; +mod search; use crate::handlers::MemoryHandler; use crate::models::*; @@ -84,6 +85,7 @@ async fn reconcile_worker(state: Arc) { let has_files = glob::glob(&pattern).map(|p| p.count() > 0).unwrap_or(false); if has_local || has_files { state.apply_sync_write(|_master| {}); + state.rebuild_index(); } let now = SystemTime::now() @@ -152,6 +154,35 @@ fn run_server(state: Arc) -> SdkResult<()> { axum::routing::get(|| async move { axum::http::StatusCode::OK }), + ).with_route( + "/dashboard", + axum::routing::get(|| async move { + axum::response::Html(include_str!("dashboard.html")) + }), + ).with_route( + "/api/stats", + axum::routing::get({ + let state_clone = state.clone(); + move || async move { + let (entities, relations) = { + let graph = state_clone.get_full_graph(); + (graph.entities.len(), graph.relations.len()) + }; + let tasks = state_clone.tasks.read().len(); + let snippets = state_clone.snippets.read().len(); + let tech_debts = state_clone.tech_debts.read().len(); + let adrs = state_clone.adrs.read().len(); + + axum::Json(serde_json::json!({ + "entities": entities, + "relations": relations, + "tasks": tasks, + "snippets": snippets, + "tech_debts": tech_debts, + "adrs": adrs + })) + } + }), ); println!("MCP Memory Server running on http://0.0.0.0:3000/sse"); @@ -190,7 +221,7 @@ fn main() -> SdkResult<()> { { // This shouldn't be executed on linux natively anymore due to workspace split, // but keeping it as a fallback. - let target_url = cli.target.as_deref().unwrap_or("http://host.docker.internal:3000"); + let target_url = cli.target.as_deref().unwrap_or("http://127.0.0.1:3000"); let _ = proxy::run_proxy(target_url); return Ok(()); } @@ -211,6 +242,7 @@ fn main() -> SdkResult<()> { session_graph: RwLock::new(KnowledgeGraph::default()), base_dir: base.clone(), master_cache: RwLock::new((KnowledgeGraph::default(), SystemTime::UNIX_EPOCH)), + search_index: RwLock::new(crate::search::MemoryIndex::new().unwrap()), ledger: Store::new(base.join("audit_ledger.json")), sticky: Store::new(base.join("sticky_notes.json")), tasks: Store::new(base.join("tasks.json")), @@ -231,6 +263,8 @@ fn main() -> SdkResult<()> { context_workspaces: Store::new(base.join("context_workspaces.json")), }); + state.rebuild_index(); + if let Some(command) = cli.command { match command { Commands::Gate { subcmd } => { diff --git a/server/src/search.rs b/server/src/search.rs index 75b7cd2..bb02a35 100644 --- a/server/src/search.rs +++ b/server/src/search.rs @@ -1,7 +1,7 @@ use tantivy::schema::*; use tantivy::{doc, Index, IndexWriter, IndexReader, ReloadPolicy}; use std::sync::Mutex; -use crate::models::{Entity, Task, StickyNote, Adr, Snippet}; +use crate::models::{Entity, Task, Adr, Snippet}; pub struct MemoryIndex { index: Index, @@ -71,21 +71,51 @@ impl MemoryIndex { Ok(()) } - pub fn search(&self, query: &str, namespace: Option<&str>) -> tantivy::Result> { + pub fn search(&self, query: &str, namespace: Option<&str>) -> tantivy::Result> { let searcher = self.reader.searcher(); let query_parser = tantivy::query::QueryParser::for_index(&self.index, vec![self.title_field, self.body_field]); let q = query_parser.parse_query(query)?; - let top_docs = searcher.search(&q, &tantivy::collector::TopDocs::with_limit(50))?; + let top_docs = searcher.search(&q, &tantivy::collector::TopDocs::with_limit(50).order_by_score())?; let mut results = Vec::new(); for (_score, doc_address) in top_docs { let retrieved_doc = searcher.doc::(doc_address)?; - if let Some(val) = retrieved_doc.get_first(self.title_field) { - if let Some(t) = val.as_str() { - results.push(t.to_string()); + let id = retrieved_doc.get_first(self.id_field).and_then(|v| v.as_str()).unwrap_or("").to_string(); + let doc_type = retrieved_doc.get_first(self.type_field).and_then(|v| v.as_str()).unwrap_or("").to_string(); + let doc_ns = retrieved_doc.get_first(self.namespace_field).and_then(|v| v.as_str()).unwrap_or(""); + if let Some(ns) = namespace { + if doc_ns != ns && doc_ns != "global" { + continue; } } + results.push((id, doc_type)); } Ok(results) } + + pub fn index_snippet(&self, s: &Snippet) -> tantivy::Result<()> { + let mut writer = self.writer.lock().unwrap(); + writer.add_document(doc!( + self.id_field => s.name.clone(), + self.title_field => s.name.clone(), + self.body_field => format!("{} {}", s.language, s.description), + self.type_field => "snippet", + self.namespace_field => "global" + ))?; + writer.commit()?; + Ok(()) + } + + pub fn index_adr(&self, a: &Adr) -> tantivy::Result<()> { + let mut writer = self.writer.lock().unwrap(); + writer.add_document(doc!( + self.id_field => a.id.clone(), + self.title_field => a.title.clone(), + self.body_field => format!("{} {} {}", a.context, a.decision, a.consequence), + self.type_field => "adr", + self.namespace_field => "global" + ))?; + writer.commit()?; + Ok(()) + } } diff --git a/server/src/state.rs b/server/src/state.rs index e12bea6..d7246f7 100644 --- a/server/src/state.rs +++ b/server/src/state.rs @@ -1,5 +1,6 @@ use crate::models::*; use crate::store::{Store, read_json_file, write_json_atomic}; +use crate::search::MemoryIndex; use std::collections::{HashMap, HashSet}; use std::fs; use std::path::PathBuf; @@ -11,6 +12,7 @@ pub struct MemoryState { pub master_path: PathBuf, pub session_graph: RwLock, pub master_cache: RwLock<(KnowledgeGraph, SystemTime)>, + pub search_index: RwLock, pub ledger: Store>, pub sticky: Store>, pub tasks: Store>, @@ -136,5 +138,27 @@ impl MemoryState { let _ = fs::remove_file(&lock_path); } + + pub fn rebuild_index(&self) { + if let Ok(new_idx) = MemoryIndex::new() { + let full = self.get_full_graph(); + for (_, e) in &full.entities { + let _ = new_idx.index_entity(e); + } + for t in self.tasks.read() { + let _ = new_idx.index_task(&t); + } + for s in self.snippets.read() { + let _ = new_idx.index_snippet(&s); + } + for a in self.adrs.read() { + let _ = new_idx.index_adr(&a); + } + if let Ok(mut w) = self.search_index.write() { + *w = new_idx; + } + } + } } +