Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f76d8ccce8 | ||
|
|
ce88bef45e |
No files matched your search
Generated
+1
@@ -1399,6 +1399,7 @@ dependencies = [
|
|||||||
"serde_json",
|
"serde_json",
|
||||||
"tantivy",
|
"tantivy",
|
||||||
"tempfile",
|
"tempfile",
|
||||||
|
"thiserror 2.0.20",
|
||||||
"tokio",
|
"tokio",
|
||||||
"tokio-stream",
|
"tokio-stream",
|
||||||
"tokio-util",
|
"tokio-util",
|
||||||
|
|||||||
Binary file not shown.
@@ -1 +0,0 @@
|
|||||||
Finished `dev` profile [unoptimized + debuginfo] target(s) in 14.64s
|
|
||||||
@@ -1,312 +0,0 @@
|
|||||||
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(<stripped>) = trimmed.strip_prefix("data: ") {
|
|
||||||
92 | if is_message {
|
|
||||||
93 ~ println!("{}", <stripped>);
|
|
||||||
94 | is_message = false;
|
|
||||||
95 | } else if is_endpoint {
|
|
||||||
96 ~ let ep = <stripped>;
|
|
||||||
|
|
|
||||||
|
|
||||||
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)
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
import re
|
|
||||||
|
|
||||||
with open("server/src/handlers.rs", "r", encoding="utf-8") as f:
|
|
||||||
text = f.read()
|
|
||||||
|
|
||||||
pattern = r'"(list_milestones|list_pinned_files|read_handoff_memos)" => \{\s*let req = parse_tool!\(args, id, ([a-zA-Z]+)\);\s*let mut [a-zA-Z_]+ = self\.state\.([a-zA-Z_]+)\.read\(\);\s*if let Some\(ns\) = req\.namespace \{\s*[a-zA-Z_]+\.retain\(\|.\| \w+\.namespace == ns\);\s*\}\s*let data = serde_json::to_string\(&[a-zA-Z_]+\)\.unwrap_or_default\(\);\s*Ok\(data\.to_string\(\)\)\s*\}'
|
|
||||||
|
|
||||||
def repl(m):
|
|
||||||
return f'"{m.group(1)}" => handle_list_with_namespace!(self, {m.group(3)}, {m.group(2)}, args, id),'
|
|
||||||
|
|
||||||
new_text = re.sub(pattern, repl, text)
|
|
||||||
|
|
||||||
with open("server/src/handlers.rs", "w", encoding="utf-8") as f:
|
|
||||||
f.write(new_text)
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
import re
|
|
||||||
|
|
||||||
with open("server/src/handlers.rs", "r", encoding="utf-8") as f:
|
|
||||||
text = f.read()
|
|
||||||
|
|
||||||
pattern = r'"([a-zA-Z_]+)" => \{\s*(?:let req = parse_tool!\(args, id, ([a-zA-Z]+)\);\s*)?let data = serde_json::to_string\(&self\.state\.([a-zA-Z_]+)\.read\(\)\)\s*\.unwrap_or_else\(\|_\| "\[\]"\.to_string\(\)\);\s*Ok\(data\.to_string\(\)\)\s*\}'
|
|
||||||
|
|
||||||
def repl(m):
|
|
||||||
return f'"{m.group(1)}" => {{\n let data = serde_json::to_string(&self.state.{m.group(3)}.read()).unwrap_or_else(|_| "[]".to_string());\n Ok(data)\n}},'
|
|
||||||
|
|
||||||
new_text = re.sub(pattern, repl, text)
|
|
||||||
|
|
||||||
with open("server/src/handlers.rs", "w", encoding="utf-8") as f:
|
|
||||||
f.write(new_text)
|
|
||||||
@@ -1,40 +0,0 @@
|
|||||||
import re
|
|
||||||
|
|
||||||
# 1. Fix stub/src/main.rs
|
|
||||||
stub_path = r'C:\Users\reazul.ashraf\workspace\rust\mcp-memory\stub\src\main.rs'
|
|
||||||
with open(stub_path, 'r', encoding='utf-8') as f:
|
|
||||||
content = f.read()
|
|
||||||
content = content.replace('use tokio::io::AsyncBufReadExt;\n', '')
|
|
||||||
with open(stub_path, 'w', encoding='utf-8') as f:
|
|
||||||
f.write(content)
|
|
||||||
|
|
||||||
# 2. Fix nvim-core/src/lib.rs
|
|
||||||
nvim_path = r'C:\Users\reazul.ashraf\workspace\rust\mcp-memory\nvim-core\src\lib.rs'
|
|
||||||
with open(nvim_path, 'r', encoding='utf-8') as f:
|
|
||||||
content = f.read()
|
|
||||||
content = content.replace(
|
|
||||||
'use tokio::io::{AsyncBufReadExt, AsyncReadExt, AsyncWriteExt, BufReader};',
|
|
||||||
'use tokio::io::{AsyncReadExt, AsyncWriteExt};'
|
|
||||||
)
|
|
||||||
with open(nvim_path, 'w', encoding='utf-8') as f:
|
|
||||||
f.write(content)
|
|
||||||
|
|
||||||
# 3. Fix server/src/store.rs
|
|
||||||
store_path = r'C:\Users\reazul.ashraf\workspace\rust\mcp-memory\server\src\store.rs'
|
|
||||||
with open(store_path, 'r', encoding='utf-8') as f:
|
|
||||||
content = f.read()
|
|
||||||
|
|
||||||
inspect_method = """ pub fn inspect<F, R>(&self, f: F) -> R
|
|
||||||
where
|
|
||||||
F: FnOnce(&T) -> R,
|
|
||||||
{
|
|
||||||
let lock = self.cache.read().unwrap();
|
|
||||||
f(&lock)
|
|
||||||
}
|
|
||||||
|
|
||||||
"""
|
|
||||||
content = content.replace(inspect_method, "")
|
|
||||||
with open(store_path, 'w', encoding='utf-8') as f:
|
|
||||||
f.write(content)
|
|
||||||
|
|
||||||
print("Warnings fixed!")
|
|
||||||
+70
-129
@@ -23,13 +23,10 @@ pub struct JsonRpcResponse {
|
|||||||
pub async fn send_response(response: JsonRpcResponse) {
|
pub async fn send_response(response: JsonRpcResponse) {
|
||||||
let msg = serde_json::to_string(&response).unwrap_or_else(|_| "{}".to_string());
|
let msg = serde_json::to_string(&response).unwrap_or_else(|_| "{}".to_string());
|
||||||
tracing::info!(
|
tracing::info!(
|
||||||
"Sending JSON-RPC response (id: {:?}): {}",
|
"Sending JSON-RPC response (id: {:?}): {}{}",
|
||||||
response.id,
|
response.id,
|
||||||
if msg.len() > 500 {
|
&msg[..std::cmp::min(msg.len(), 500)],
|
||||||
format!("{}...", &msg[..500])
|
if msg.len() > 500 { "..." } else { "" }
|
||||||
} else {
|
|
||||||
msg.clone()
|
|
||||||
}
|
|
||||||
);
|
);
|
||||||
// CRITICAL ARCHITECTURAL DECISION:
|
// CRITICAL ARCHITECTURAL DECISION:
|
||||||
// The MCP StdioTransport MUST use Newline-Delimited JSON (NDJSON).
|
// The MCP StdioTransport MUST use Newline-Delimited JSON (NDJSON).
|
||||||
@@ -208,16 +205,20 @@ async fn get_nvim_connection() -> Result<mpsc::Sender<NvimRequest>, String> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Trim buffer if it gets too large
|
if offset == resp_buf.len() {
|
||||||
if offset > 1024 * 1024 {
|
resp_buf.clear();
|
||||||
|
offset = 0;
|
||||||
|
} else if offset > 1024 * 1024 {
|
||||||
resp_buf.drain(..offset);
|
resp_buf.drain(..offset);
|
||||||
offset = 0;
|
offset = 0;
|
||||||
}
|
}
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
Err(rmpv::decode::Error::InvalidMarkerRead(e))
|
Err(e) if match &e {
|
||||||
if e.kind() == std::io::ErrorKind::UnexpectedEof =>
|
rmpv::decode::Error::InvalidMarkerRead(io_err) => io_err.kind() == std::io::ErrorKind::UnexpectedEof,
|
||||||
{
|
rmpv::decode::Error::InvalidDataRead(io_err) => io_err.kind() == std::io::ErrorKind::UnexpectedEof,
|
||||||
|
_ => false,
|
||||||
|
} => {
|
||||||
resp_buf.drain(..offset);
|
resp_buf.drain(..offset);
|
||||||
offset = 0;
|
offset = 0;
|
||||||
|
|
||||||
@@ -305,86 +306,68 @@ async fn call_nvim(req: rmpv::Value) -> Result<rmpv::Value, String> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn send_nvim_command(cmd: &str) -> Result<(), String> {
|
async fn call_nvim_method(method: &str, args: Vec<rmpv::Value>) -> Result<rmpv::Value, String> {
|
||||||
use rmpv::Value as RmpValue;
|
use rmpv::Value as RmpValue;
|
||||||
let msgid = NEXT_MSGID.fetch_add(1, Ordering::SeqCst);
|
let msgid = NEXT_MSGID.fetch_add(1, Ordering::SeqCst);
|
||||||
let req = RmpValue::Array(vec![
|
let req = RmpValue::Array(vec![
|
||||||
RmpValue::Integer(0.into()),
|
RmpValue::Integer(0.into()),
|
||||||
RmpValue::Integer(msgid.into()), // msgid
|
RmpValue::Integer(msgid.into()),
|
||||||
RmpValue::String("nvim_command".into()),
|
RmpValue::String(method.into()),
|
||||||
RmpValue::Array(vec![RmpValue::String(cmd.into())]),
|
RmpValue::Array(args),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
let resp = call_nvim(req).await?;
|
let resp = call_nvim(req).await?;
|
||||||
if let RmpValue::Array(arr) = resp {
|
if let RmpValue::Array(mut arr) = resp {
|
||||||
|
if arr.len() < 4 {
|
||||||
|
return Err("Invalid response length".to_string());
|
||||||
|
}
|
||||||
if !arr[2].is_nil() {
|
if !arr[2].is_nil() {
|
||||||
return Err(format!("Neovim error: {:?}", arr[2]));
|
return Err(format!("Neovim error: {:?}", arr[2]));
|
||||||
}
|
}
|
||||||
return Ok(());
|
return Ok(arr.swap_remove(3));
|
||||||
}
|
}
|
||||||
Err("Invalid response".to_string())
|
Err("Invalid response format".to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn send_nvim_command(cmd: &str) -> Result<(), String> {
|
||||||
|
call_nvim_method("nvim_command", vec![rmpv::Value::String(cmd.into())]).await?;
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn get_nvim_active_buffer() -> Result<String, String> {
|
async fn get_nvim_active_buffer() -> Result<String, String> {
|
||||||
use rmpv::Value as RmpValue;
|
let result = call_nvim_method("nvim_buf_get_lines", vec![
|
||||||
let msgid = NEXT_MSGID.fetch_add(1, Ordering::SeqCst);
|
rmpv::Value::Integer(0.into()),
|
||||||
let req = RmpValue::Array(vec![
|
rmpv::Value::Integer(0.into()),
|
||||||
RmpValue::Integer(0.into()),
|
rmpv::Value::Integer((-1).into()),
|
||||||
RmpValue::Integer(msgid.into()), // msgid
|
rmpv::Value::Boolean(true),
|
||||||
RmpValue::String("nvim_buf_get_lines".into()),
|
]).await?;
|
||||||
RmpValue::Array(vec![
|
|
||||||
RmpValue::Integer(0.into()),
|
if let rmpv::Value::Array(lines) = result {
|
||||||
RmpValue::Integer(0.into()),
|
let mut text = String::new();
|
||||||
RmpValue::Integer((-1).into()),
|
for line in lines {
|
||||||
RmpValue::Boolean(true),
|
if let rmpv::Value::String(s) = line {
|
||||||
]),
|
if let Some(s) = s.as_str() {
|
||||||
]);
|
text.push_str(s);
|
||||||
|
text.push('\n');
|
||||||
let resp = call_nvim(req).await?;
|
|
||||||
if let RmpValue::Array(arr) = resp {
|
|
||||||
if !arr[2].is_nil() {
|
|
||||||
return Err(format!("Neovim error: {:?}", arr[2]));
|
|
||||||
}
|
|
||||||
if let RmpValue::Array(lines) = &arr[3] {
|
|
||||||
let mut text = String::new();
|
|
||||||
for line in lines {
|
|
||||||
if let RmpValue::String(s) = line {
|
|
||||||
if let Some(s) = s.as_str() {
|
|
||||||
text.push_str(s);
|
|
||||||
text.push('\n');
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return Ok(text);
|
|
||||||
}
|
}
|
||||||
|
return Ok(text);
|
||||||
}
|
}
|
||||||
Err("Invalid response".to_string())
|
Err("Invalid response format".to_string())
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn get_nvim_cursor() -> Result<String, String> {
|
async fn get_nvim_cursor() -> Result<String, String> {
|
||||||
use rmpv::Value as RmpValue;
|
let result = call_nvim_method("nvim_win_get_cursor", vec![rmpv::Value::Integer(0.into())]).await?;
|
||||||
let msgid = NEXT_MSGID.fetch_add(1, Ordering::SeqCst);
|
|
||||||
let req = RmpValue::Array(vec![
|
if let rmpv::Value::Array(pos) = result {
|
||||||
RmpValue::Integer(0.into()),
|
if pos.len() == 2 {
|
||||||
RmpValue::Integer(msgid.into()), // msgid
|
if let (rmpv::Value::Integer(row), rmpv::Value::Integer(col)) = (&pos[0], &pos[1]) {
|
||||||
RmpValue::String("nvim_win_get_cursor".into()),
|
return Ok(format!("Line: {row}, Column: {col}"));
|
||||||
RmpValue::Array(vec![RmpValue::Integer(0.into())]),
|
|
||||||
]);
|
|
||||||
|
|
||||||
let resp = call_nvim(req).await?;
|
|
||||||
if let RmpValue::Array(arr) = resp {
|
|
||||||
if !arr[2].is_nil() {
|
|
||||||
return Err(format!("Neovim error: {:?}", arr[2]));
|
|
||||||
}
|
|
||||||
if let RmpValue::Array(pos) = &arr[3] {
|
|
||||||
if pos.len() == 2 {
|
|
||||||
if let (RmpValue::Integer(row), RmpValue::Integer(col)) = (&pos[0], &pos[1]) {
|
|
||||||
return Ok(format!("Line: {row}, Column: {col}"));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Err("Invalid response".to_string())
|
Err("Invalid response format".to_string())
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn get_nvim_visual_selection() -> Result<String, String> {
|
async fn get_nvim_visual_selection() -> Result<String, String> {
|
||||||
@@ -399,30 +382,17 @@ async fn get_nvim_visual_selection() -> Result<String, String> {
|
|||||||
end
|
end
|
||||||
"#;
|
"#;
|
||||||
|
|
||||||
use rmpv::Value as RmpValue;
|
let result = call_nvim_method("nvim_exec_lua", vec![
|
||||||
let msgid = NEXT_MSGID.fetch_add(1, Ordering::SeqCst);
|
rmpv::Value::String(lua_script.into()),
|
||||||
let req = RmpValue::Array(vec![
|
rmpv::Value::Array(vec![]),
|
||||||
RmpValue::Integer(0.into()),
|
]).await?;
|
||||||
RmpValue::Integer(msgid.into()), // msgid
|
|
||||||
RmpValue::String("nvim_exec_lua".into()),
|
if let rmpv::Value::String(s) = result {
|
||||||
RmpValue::Array(vec![
|
if let Some(text) = s.as_str() {
|
||||||
RmpValue::String(lua_script.into()),
|
return Ok(text.to_string());
|
||||||
RmpValue::Array(vec![]),
|
|
||||||
]),
|
|
||||||
]);
|
|
||||||
|
|
||||||
let resp = call_nvim(req).await?;
|
|
||||||
if let RmpValue::Array(arr) = resp {
|
|
||||||
if !arr[2].is_nil() {
|
|
||||||
return Err(format!("Neovim error: {:?}", arr[2]));
|
|
||||||
}
|
|
||||||
if let RmpValue::String(s) = &arr[3] {
|
|
||||||
if let Some(text) = s.as_str() {
|
|
||||||
return Ok(text.to_string());
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Err("Invalid response".to_string())
|
Err("Invalid response format".to_string())
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn set_nvim_diagnostics(line: i64, message: &str) -> Result<(), String> {
|
async fn set_nvim_diagnostics(line: i64, message: &str) -> Result<(), String> {
|
||||||
@@ -440,26 +410,12 @@ async fn set_nvim_diagnostics(line: i64, message: &str) -> Result<(), String> {
|
|||||||
"#
|
"#
|
||||||
);
|
);
|
||||||
|
|
||||||
use rmpv::Value as RmpValue;
|
call_nvim_method("nvim_exec_lua", vec![
|
||||||
let msgid = NEXT_MSGID.fetch_add(1, Ordering::SeqCst);
|
rmpv::Value::String(lua_script.into()),
|
||||||
let req = RmpValue::Array(vec![
|
rmpv::Value::Array(vec![]),
|
||||||
RmpValue::Integer(0.into()),
|
]).await?;
|
||||||
RmpValue::Integer(msgid.into()), // msgid
|
|
||||||
RmpValue::String("nvim_exec_lua".into()),
|
Ok(())
|
||||||
RmpValue::Array(vec![
|
|
||||||
RmpValue::String(lua_script.into()),
|
|
||||||
RmpValue::Array(vec![]),
|
|
||||||
]),
|
|
||||||
]);
|
|
||||||
|
|
||||||
let resp = call_nvim(req).await?;
|
|
||||||
if let RmpValue::Array(arr) = resp {
|
|
||||||
if !arr[2].is_nil() {
|
|
||||||
return Err(format!("Neovim error: {:?}", arr[2]));
|
|
||||||
}
|
|
||||||
return Ok(());
|
|
||||||
}
|
|
||||||
Err("Invalid response".to_string())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn rmpv_to_json(val: &rmpv::Value) -> serde_json::Value {
|
fn rmpv_to_json(val: &rmpv::Value) -> serde_json::Value {
|
||||||
@@ -505,26 +461,11 @@ fn rmpv_to_json(val: &rmpv::Value) -> serde_json::Value {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async fn execute_nvim_lua(code: &str) -> Result<String, String> {
|
async fn execute_nvim_lua(code: &str) -> Result<String, String> {
|
||||||
use rmpv::Value as RmpValue;
|
let result = call_nvim_method("nvim_exec_lua", vec![
|
||||||
let msgid = NEXT_MSGID.fetch_add(1, Ordering::SeqCst);
|
rmpv::Value::String(code.into()),
|
||||||
let req = RmpValue::Array(vec![
|
rmpv::Value::Array(vec![]),
|
||||||
RmpValue::Integer(0.into()),
|
]).await?;
|
||||||
RmpValue::Integer(msgid.into()), // msgid
|
Ok(serde_json::to_string_pretty(&rmpv_to_json(&result)).unwrap_or_default())
|
||||||
RmpValue::String("nvim_exec_lua".into()),
|
|
||||||
RmpValue::Array(vec![RmpValue::String(code.into()), RmpValue::Array(vec![])]),
|
|
||||||
]);
|
|
||||||
|
|
||||||
let resp = call_nvim(req).await?;
|
|
||||||
if let RmpValue::Array(arr) = resp {
|
|
||||||
if !arr[2].is_nil() {
|
|
||||||
return Err(format!("Neovim error: {:?}", arr[2]));
|
|
||||||
}
|
|
||||||
if arr.len() > 3 {
|
|
||||||
return Ok(serde_json::to_string_pretty(&rmpv_to_json(&arr[3])).unwrap_or_default());
|
|
||||||
}
|
|
||||||
return Ok(String::new());
|
|
||||||
}
|
|
||||||
Err("Invalid response".to_string())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
macro_rules! send_text_result {
|
macro_rules! send_text_result {
|
||||||
|
|||||||
-168
@@ -1,168 +0,0 @@
|
|||||||
import re
|
|
||||||
import sys
|
|
||||||
|
|
||||||
with open('C:\\Users\\reazul.ashraf\\workspace\\rust\\mcp-memory\\nvim-core\\src\\lib.rs', 'r', encoding='utf-8') as f:
|
|
||||||
content = f.read()
|
|
||||||
|
|
||||||
start_pattern = r'#\[cfg\(windows\)\]\nasync fn call_nvim'
|
|
||||||
end_pattern = r'async fn send_nvim_command'
|
|
||||||
|
|
||||||
start_idx = re.search(start_pattern, content).start()
|
|
||||||
end_idx = re.search(end_pattern, content).start()
|
|
||||||
|
|
||||||
new_code = """use std::sync::LazyLock;
|
|
||||||
use std::sync::Arc;
|
|
||||||
use tokio::sync::{Mutex, mpsc, oneshot};
|
|
||||||
use std::collections::HashMap;
|
|
||||||
use tokio::io::{AsyncReadExt, AsyncWriteExt};
|
|
||||||
|
|
||||||
pub struct NvimRequest {
|
|
||||||
pub msgid_str: String,
|
|
||||||
pub req: rmpv::Value,
|
|
||||||
pub reply: oneshot::Sender<Result<rmpv::Value, String>>,
|
|
||||||
}
|
|
||||||
|
|
||||||
static NVIM_CONN: LazyLock<Arc<Mutex<Option<mpsc::Sender<NvimRequest>>>>> = LazyLock::new(|| Arc::new(Mutex::new(None)));
|
|
||||||
|
|
||||||
async fn get_nvim_connection() -> Result<mpsc::Sender<NvimRequest>, String> {
|
|
||||||
let mut conn_lock = NVIM_CONN.lock().await;
|
|
||||||
if let Some(sender) = conn_lock.as_ref() {
|
|
||||||
if !sender.is_closed() {
|
|
||||||
return Ok(sender.clone());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
tracing::info!("Establishing new persistent connection to Neovim");
|
|
||||||
let socket_path = get_socket_path().await?;
|
|
||||||
|
|
||||||
#[cfg(windows)]
|
|
||||||
let stream = {
|
|
||||||
use tokio::net::windows::named_pipe::ClientOptions;
|
|
||||||
ClientOptions::new().open(&socket_path).map_err(|e| e.to_string())?
|
|
||||||
};
|
|
||||||
|
|
||||||
#[cfg(unix)]
|
|
||||||
let stream = {
|
|
||||||
use tokio::net::UnixStream;
|
|
||||||
UnixStream::connect(socket_path).await.map_err(|e| e.to_string())?
|
|
||||||
};
|
|
||||||
|
|
||||||
let (mut read_half, mut write_half) = tokio::io::split(stream);
|
|
||||||
let (tx, mut rx) = mpsc::channel::<NvimRequest>(32);
|
|
||||||
let pending_requests: Arc<Mutex<HashMap<String, oneshot::Sender<Result<rmpv::Value, String>>>>> = Arc::new(Mutex::new(HashMap::new()));
|
|
||||||
|
|
||||||
// Write task
|
|
||||||
let pending_clone = Arc::clone(&pending_requests);
|
|
||||||
tokio::spawn(async move {
|
|
||||||
while let Some(req) = rx.recv().await {
|
|
||||||
let mut buf = Vec::new();
|
|
||||||
if let Err(e) = rmpv::encode::write_value(&mut buf, &req.req) {
|
|
||||||
let _ = req.reply.send(Err(e.to_string()));
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
pending_clone.lock().await.insert(req.msgid_str.clone(), req.reply);
|
|
||||||
|
|
||||||
if write_half.write_all(&buf).await.is_err() {
|
|
||||||
tracing::error!("Failed to write to Neovim socket");
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Read task
|
|
||||||
let pending_clone2 = Arc::clone(&pending_requests);
|
|
||||||
tokio::spawn(async move {
|
|
||||||
let mut resp_buf = Vec::new();
|
|
||||||
let mut chunk = vec![0u8; 8192];
|
|
||||||
let mut offset = 0;
|
|
||||||
|
|
||||||
loop {
|
|
||||||
let mut cursor = std::io::Cursor::new(&resp_buf[offset..]);
|
|
||||||
match rmpv::decode::read_value(&mut cursor) {
|
|
||||||
Ok(val) => {
|
|
||||||
offset += cursor.position() as usize;
|
|
||||||
|
|
||||||
if let rmpv::Value::Array(ref arr) = val {
|
|
||||||
if arr.len() >= 4 && arr[0] == rmpv::Value::Integer(1.into()) {
|
|
||||||
let msgid = &arr[1];
|
|
||||||
let msgid_str = format!("{:?}", msgid);
|
|
||||||
|
|
||||||
if let Some(reply_sender) = pending_clone2.lock().await.remove(&msgid_str) {
|
|
||||||
let _ = reply_sender.send(Ok(val));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Trim buffer if it gets too large
|
|
||||||
if offset > 1024 * 1024 {
|
|
||||||
resp_buf.drain(..offset);
|
|
||||||
offset = 0;
|
|
||||||
}
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
Err(_) => {
|
|
||||||
if offset > 0 {
|
|
||||||
resp_buf.drain(..offset);
|
|
||||||
offset = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
let read_future = read_half.read(&mut chunk);
|
|
||||||
match tokio::time::timeout(tokio::time::Duration::from_secs(60), read_future).await {
|
|
||||||
Ok(Ok(n)) if n > 0 => {
|
|
||||||
resp_buf.extend_from_slice(&chunk[..n]);
|
|
||||||
}
|
|
||||||
_ => {
|
|
||||||
tracing::error!("Neovim socket read loop closed or timeout");
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Cleanup pending requests on disconnect
|
|
||||||
let mut pending = pending_clone2.lock().await;
|
|
||||||
for (_, sender) in pending.drain() {
|
|
||||||
let _ = sender.send(Err("Connection closed".to_string()));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
*conn_lock = Some(tx.clone());
|
|
||||||
Ok(tx)
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn call_nvim(req: rmpv::Value) -> Result<rmpv::Value, String> {
|
|
||||||
let msgid = if let rmpv::Value::Array(ref arr) = req {
|
|
||||||
if arr.len() > 1 {
|
|
||||||
arr[1].clone()
|
|
||||||
} else {
|
|
||||||
rmpv::Value::Nil
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
rmpv::Value::Nil
|
|
||||||
};
|
|
||||||
|
|
||||||
let msgid_str = format!("{:?}", msgid);
|
|
||||||
let tx = get_nvim_connection().await?;
|
|
||||||
let (reply_tx, reply_rx) = oneshot::channel();
|
|
||||||
|
|
||||||
tx.send(NvimRequest {
|
|
||||||
msgid_str,
|
|
||||||
req,
|
|
||||||
reply: reply_tx,
|
|
||||||
}).await.map_err(|_| "Failed to send request to Neovim connection manager")?;
|
|
||||||
|
|
||||||
match tokio::time::timeout(tokio::time::Duration::from_secs(10), reply_rx).await {
|
|
||||||
Ok(Ok(res)) => res,
|
|
||||||
Ok(Err(_)) => Err("Response channel dropped".to_string()),
|
|
||||||
Err(_) => Err("Timeout waiting for Neovim response".to_string()),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
"""
|
|
||||||
|
|
||||||
new_content = content[:start_idx] + new_code + content[end_idx:]
|
|
||||||
with open('C:\\Users\\reazul.ashraf\\workspace\\rust\\mcp-memory\\nvim-core\\src\\lib.rs', 'w', encoding='utf-8') as f:
|
|
||||||
f.write(new_content)
|
|
||||||
|
|
||||||
print("Patched!")
|
|
||||||
@@ -1,40 +0,0 @@
|
|||||||
import re
|
|
||||||
import sys
|
|
||||||
|
|
||||||
with open('C:\\Users\\reazul.ashraf\\workspace\\rust\\mcp-memory\\nvim-core\\src\\lib.rs', 'r', encoding='utf-8') as f:
|
|
||||||
content = f.read()
|
|
||||||
|
|
||||||
macro_def = """
|
|
||||||
macro_rules! send_text_result {
|
|
||||||
($id:expr, $text:expr) => {
|
|
||||||
send_response(JsonRpcResponse {
|
|
||||||
jsonrpc: "2.0".to_string(),
|
|
||||||
id: $id,
|
|
||||||
result: Some(json!({
|
|
||||||
"content": [{"type": "text", "text": $text}]
|
|
||||||
})),
|
|
||||||
error: None,
|
|
||||||
}).await;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
"""
|
|
||||||
|
|
||||||
if "macro_rules! send_text_result" not in content:
|
|
||||||
idx = content.find("pub async fn run_mcp_loop")
|
|
||||||
content = content[:idx] + macro_def + "\n" + content[idx:]
|
|
||||||
|
|
||||||
# Find tools/call block
|
|
||||||
tools_call_idx = content.find('"tools/call" => {')
|
|
||||||
if tools_call_idx != -1:
|
|
||||||
block = content[tools_call_idx:]
|
|
||||||
|
|
||||||
# Replace the verbose blocks
|
|
||||||
pattern = r'send_response\(JsonRpcResponse \{\s*jsonrpc: "2\.0"\.to_string\(\),\s*id,\s*result: Some\(json!\(\{\s*"content": \[\{"type": "text", "text": (.*?)\}\]\s*\}\)\),\s*error: None,\s*\}\)\s*\.await;'
|
|
||||||
|
|
||||||
new_block = re.sub(pattern, r'send_text_result!(id.clone(), \1);', block)
|
|
||||||
|
|
||||||
content = content[:tools_call_idx] + new_block
|
|
||||||
|
|
||||||
with open('C:\\Users\\reazul.ashraf\\workspace\\rust\\mcp-memory\\nvim-core\\src\\lib.rs', 'w', encoding='utf-8') as f:
|
|
||||||
f.write(content)
|
|
||||||
print("Patched nvim macro")
|
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
import re
|
|
||||||
import sys
|
|
||||||
|
|
||||||
with open('C:\\Users\\reazul.ashraf\\workspace\\rust\\mcp-memory\\stub\\src\\main.rs', 'r', encoding='utf-8') as f:
|
|
||||||
content = f.read()
|
|
||||||
|
|
||||||
start1 = r'let mut log_prefix = String::from\("Message"\);\n\s*if let Ok\(v\) = serde_json::from_str::<serde_json::Value>\(&msg\) \{[\s\S]*?log_prefix = format!\("Response id=\{\}", res_id\);\n\s*\}\n\s*\}'
|
|
||||||
content = re.sub(start1, r'let log_prefix = logger::extract_log_prefix(&msg, false);', content)
|
|
||||||
|
|
||||||
start2 = r'let mut log_prefix = String::from\("Message"\);\n\s*if let Ok\(v\) = serde_json::from_str::<serde_json::Value>\(&text\) \{[\s\S]*?log_prefix = format!\("Response id=\{\} \[Error: \{\}\]", id, is_error\);\n\s*\}'
|
|
||||||
content = re.sub(start2, r'let log_prefix = logger::extract_log_prefix(&text, true);', content)
|
|
||||||
|
|
||||||
with open('C:\\Users\\reazul.ashraf\\workspace\\rust\\mcp-memory\\stub\\src\\main.rs', 'w', encoding='utf-8') as f:
|
|
||||||
f.write(content)
|
|
||||||
print("Patched stub")
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
import re
|
|
||||||
|
|
||||||
with open('C:\\Users\\reazul.ashraf\\workspace\\rust\\mcp-memory\\nvim-core\\src\\lib.rs', 'r', encoding='utf-8') as f:
|
|
||||||
content = f.read()
|
|
||||||
|
|
||||||
content = content.replace('read_message(&mut reader)', 'mcp_stdio::read_mcp_message(&mut reader)')
|
|
||||||
content = content.replace('let req = mcp_stdio::read_mcp_message(&mut reader).await.unwrap();', 'let raw = mcp_stdio::read_mcp_message(&mut reader).await.unwrap();\n let req: JsonRpcRequest = serde_json::from_str(&raw).unwrap();')
|
|
||||||
content = content.replace('let req = mcp_stdio::read_mcp_message(&mut reader).await;', 'let req = mcp_stdio::read_mcp_message(&mut reader).await;')
|
|
||||||
|
|
||||||
with open('C:\\Users\\reazul.ashraf\\workspace\\rust\\mcp-memory\\nvim-core\\src\\lib.rs', 'w', encoding='utf-8') as f:
|
|
||||||
f.write(content)
|
|
||||||
print("Patched tests")
|
|
||||||
+2
-6
@@ -15,7 +15,7 @@ glob = "0.3.4"
|
|||||||
git2 = { version = "0.19.0", default-features = false }
|
git2 = { version = "0.19.0", default-features = false }
|
||||||
notify = "6.1.1"
|
notify = "6.1.1"
|
||||||
redb = "4.2.0"
|
redb = "4.2.0"
|
||||||
reqwest = { version = "0.12", default-features = false, features = ["stream", "rustls-tls"] }
|
reqwest = { version = "0.12", default-features = false, features = ["stream", "rustls-tls", "json"] }
|
||||||
schemars = "1.2.2"
|
schemars = "1.2.2"
|
||||||
serde = { version = "1.0.229", features = ["derive"] }
|
serde = { version = "1.0.229", features = ["derive"] }
|
||||||
serde_json = "1.0.151"
|
serde_json = "1.0.151"
|
||||||
@@ -28,6 +28,7 @@ tracing-subscriber = "0.3.23"
|
|||||||
uuid = { version = "1.26.0", features = ["v4"] }
|
uuid = { version = "1.26.0", features = ["v4"] }
|
||||||
tracing-appender = "0.2.5"
|
tracing-appender = "0.2.5"
|
||||||
rmcp = { version = "3.4.0", features = ["server"] }
|
rmcp = { version = "3.4.0", features = ["server"] }
|
||||||
|
thiserror = "2.0.20"
|
||||||
|
|
||||||
[build-dependencies]
|
[build-dependencies]
|
||||||
chrono = "0.4.45"
|
chrono = "0.4.45"
|
||||||
@@ -35,8 +36,3 @@ winres = "0.1.12"
|
|||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
tempfile = "3.27.0"
|
tempfile = "3.27.0"
|
||||||
|
|
||||||
[[bin]]
|
|
||||||
name = "test_rmcp"
|
|
||||||
path = "src/bin_test.rs"
|
|
||||||
|
|
||||||
@@ -1,24 +0,0 @@
|
|||||||
import re
|
|
||||||
|
|
||||||
with open(r'C:\Users\reazul.ashraf\workspace\rust\mcp-memory\server\src\handlers.rs', 'r', encoding='utf-8') as f:
|
|
||||||
content = f.read()
|
|
||||||
|
|
||||||
content = content.replace(
|
|
||||||
'crate::mcp::tool_def::<GenerateStandupReportTool>(\n "generate_standup_report",\n "",\n )',
|
|
||||||
'crate::mcp::tool_def::<GenerateStandupReportTool>(\n "generate_standup_report",\n "Generate a standup report summarizing recent work, blockers, and next steps.",\n )'
|
|
||||||
)
|
|
||||||
|
|
||||||
content = content.replace(
|
|
||||||
'crate::mcp::tool_def::<GetEnvironmentDetailsTool>(\n "get_environment_details",\n "",\n )',
|
|
||||||
'crate::mcp::tool_def::<GetEnvironmentDetailsTool>(\n "get_environment_details",\n "Get detailed information about a specific deployment environment.",\n )'
|
|
||||||
)
|
|
||||||
|
|
||||||
content = content.replace(
|
|
||||||
'crate::mcp::tool_def::<ListContextWorkspacesTool>(\n "list_context_workspaces",\n "",\n )',
|
|
||||||
'crate::mcp::tool_def::<ListContextWorkspacesTool>(\n "list_context_workspaces",\n "List all saved context workspaces.",\n )'
|
|
||||||
)
|
|
||||||
|
|
||||||
with open(r'C:\Users\reazul.ashraf\workspace\rust\mcp-memory\server\src\handlers.rs', 'w', encoding='utf-8') as f:
|
|
||||||
f.write(content)
|
|
||||||
|
|
||||||
print("Descriptions fixed.")
|
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
pub mod rest;
|
||||||
|
pub mod setup;
|
||||||
|
pub mod telemetry;
|
||||||
|
pub mod ws;
|
||||||
@@ -0,0 +1,107 @@
|
|||||||
|
use crate::AppState;
|
||||||
|
use crate::error::AppError;
|
||||||
|
use crate::models::GateRecord;
|
||||||
|
use axum::{
|
||||||
|
Json,
|
||||||
|
extract::{Query, State},
|
||||||
|
response::IntoResponse,
|
||||||
|
};
|
||||||
|
use std::collections::HashMap;
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
#[derive(serde::Deserialize, serde::Serialize)]
|
||||||
|
pub struct GateVerifyReq {
|
||||||
|
pub action: String,
|
||||||
|
pub target: String,
|
||||||
|
pub namespace: Option<String>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub params: HashMap<String, String>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub consume: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(serde::Deserialize, serde::Serialize)]
|
||||||
|
pub struct GateSetReq {
|
||||||
|
pub action: String,
|
||||||
|
pub target: String,
|
||||||
|
pub namespace: Option<String>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub params: HashMap<String, String>,
|
||||||
|
pub authorize: Option<bool>,
|
||||||
|
pub block: Option<bool>,
|
||||||
|
pub reason: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn gate_verify_handler(
|
||||||
|
State(app_state): State<Arc<AppState>>,
|
||||||
|
Query(q): Query<GateVerifyReq>,
|
||||||
|
) -> Result<impl IntoResponse, AppError> {
|
||||||
|
let mut found = None;
|
||||||
|
let mut to_remove = None;
|
||||||
|
app_state.handler.state.gates.modify(|gates| {
|
||||||
|
if let Some(idx) = gates.iter().position(|g| {
|
||||||
|
g.action == q.action
|
||||||
|
&& g.target == q.target
|
||||||
|
&& g.namespace == q.namespace
|
||||||
|
&& g.params == q.params
|
||||||
|
}) {
|
||||||
|
found = Some(gates[idx].clone());
|
||||||
|
if q.consume {
|
||||||
|
to_remove = Some(idx);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if let Some(idx) = to_remove {
|
||||||
|
gates.remove(idx);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
match found {
|
||||||
|
Some(record) => {
|
||||||
|
if record.status == "authorized" {
|
||||||
|
Ok((axum::http::StatusCode::OK, "Authorized"))
|
||||||
|
} else {
|
||||||
|
let msg = if let Some(r) = record.reason {
|
||||||
|
format!("Action blocked. Reason: {}", r)
|
||||||
|
} else {
|
||||||
|
"Action blocked.".to_string()
|
||||||
|
};
|
||||||
|
Err(AppError::Forbidden(msg))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None => Err(AppError::NotFound("Action not yet authorized (no gate record found).".to_string())),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn gate_set_handler(
|
||||||
|
State(app_state): State<Arc<AppState>>,
|
||||||
|
Json(body): Json<GateSetReq>,
|
||||||
|
) -> Result<impl IntoResponse, AppError> {
|
||||||
|
let status = if body.block.unwrap_or(false) {
|
||||||
|
"blocked".to_string()
|
||||||
|
} else if body.authorize.unwrap_or(false) {
|
||||||
|
"authorized".to_string()
|
||||||
|
} else {
|
||||||
|
"pending".to_string()
|
||||||
|
};
|
||||||
|
|
||||||
|
let record = GateRecord {
|
||||||
|
id: uuid::Uuid::new_v4().to_string(),
|
||||||
|
action: body.action.clone(),
|
||||||
|
target: body.target.clone(),
|
||||||
|
namespace: body.namespace.clone(),
|
||||||
|
params: body.params.clone(),
|
||||||
|
status,
|
||||||
|
reason: body.reason.clone(),
|
||||||
|
timestamp: crate::handlers::utils::now_secs(),
|
||||||
|
};
|
||||||
|
app_state.handler.state.gates.modify(|gates| {
|
||||||
|
gates.retain(|g| !(g.action == record.action && g.target == record.target));
|
||||||
|
gates.push(record);
|
||||||
|
});
|
||||||
|
|
||||||
|
Ok((axum::http::StatusCode::OK, "Gate state updated."))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn health_handler() -> &'static str {
|
||||||
|
"OK"
|
||||||
|
}
|
||||||
@@ -0,0 +1,301 @@
|
|||||||
|
if let Some(idx) = gates.iter().position(|g| {
|
||||||
|
g.action == q.action
|
||||||
|
&& g.target == q.target
|
||||||
|
&& g.namespace == q.namespace
|
||||||
|
&& g.params == q.params
|
||||||
|
}) {
|
||||||
|
found = Some(gates[idx].clone());
|
||||||
|
if q.consume {
|
||||||
|
to_remove = Some(idx);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if let Some(idx) = to_remove {
|
||||||
|
gates.remove(idx);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
match found {
|
||||||
|
Some(record) => {
|
||||||
|
if record.status == "authorized" {
|
||||||
|
(axum::http::StatusCode::OK, "Authorized").into_response()
|
||||||
|
} else {
|
||||||
|
let msg = if let Some(r) = record.reason {
|
||||||
|
format!("Action blocked. Reason: {}", r)
|
||||||
|
} else {
|
||||||
|
"Action blocked.".to_string()
|
||||||
|
};
|
||||||
|
(axum::http::StatusCode::FORBIDDEN, msg).into_response()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None => (
|
||||||
|
axum::http::StatusCode::NOT_FOUND,
|
||||||
|
"Action not yet authorized (no gate record found).",
|
||||||
|
)
|
||||||
|
.into_response(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn gate_set_handler(
|
||||||
|
State(app_state): State<Arc<AppState>>,
|
||||||
|
Json(body): Json<GateSetReq>,
|
||||||
|
) -> axum::response::Response {
|
||||||
|
let status = if body.block.unwrap_or(false) {
|
||||||
|
"blocked".to_string()
|
||||||
|
} else if body.authorize.unwrap_or(false) {
|
||||||
|
"authorized".to_string()
|
||||||
|
} else {
|
||||||
|
"pending".to_string()
|
||||||
|
};
|
||||||
|
|
||||||
|
let record = GateRecord {
|
||||||
|
id: uuid::Uuid::new_v4().to_string(),
|
||||||
|
action: body.action.clone(),
|
||||||
|
target: body.target.clone(),
|
||||||
|
namespace: body.namespace.clone(),
|
||||||
|
params: body.params.clone(),
|
||||||
|
status,
|
||||||
|
reason: body.reason.clone(),
|
||||||
|
timestamp: crate::handlers::utils::now_secs(),
|
||||||
|
};
|
||||||
|
app_state.handler.state.gates.modify(|gates| {
|
||||||
|
gates.retain(|g| !(g.action == record.action && g.target == record.target));
|
||||||
|
gates.push(record);
|
||||||
|
});
|
||||||
|
(axum::http::StatusCode::OK, "Gate state updated.").into_response()
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn run_server(state: Arc<MemoryState>) -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
state.rebuild_index().await;
|
||||||
|
tokio::spawn(index_committer_worker(Arc::clone(&state)));
|
||||||
|
let app_state = Arc::new(AppState {
|
||||||
|
handler: Arc::new(MemoryHandler::new(Arc::clone(&state))),
|
||||||
|
clients: RwLock::new(HashMap::new()),
|
||||||
|
next_id: AtomicUsize::new(1),
|
||||||
|
});
|
||||||
|
|
||||||
|
let app_state_clone = Arc::clone(&app_state);
|
||||||
|
let mut rx = state.activity_tx.subscribe();
|
||||||
|
tokio::spawn(async move {
|
||||||
|
while let Ok(msg) = rx.recv().await {
|
||||||
|
let senders: Vec<_> = app_state_clone
|
||||||
|
.clients
|
||||||
|
.read()
|
||||||
|
.unwrap_or_else(|e| e.into_inner())
|
||||||
|
.values()
|
||||||
|
.cloned()
|
||||||
|
.collect();
|
||||||
|
for client_tx in senders {
|
||||||
|
let _ = client_tx.try_send(msg.clone());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
let app = Router::new()
|
||||||
|
.route(
|
||||||
|
"/api/version",
|
||||||
|
get(|| async move {
|
||||||
|
axum::Json(serde_json::json!({
|
||||||
|
"version": env!("APP_VERSION"),
|
||||||
|
"git_hash": option_env!("GIT_HASH").unwrap_or("unknown")
|
||||||
|
}))
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.route("/ws", get(ws_handler))
|
||||||
|
.route("/health", get(health_handler))
|
||||||
|
.route("/nvim/telemetry", post(nvim_telemetry_handler))
|
||||||
|
.route("/gate/verify", get(gate_verify_handler))
|
||||||
|
.route("/gate/set", post(gate_set_handler))
|
||||||
|
.route(
|
||||||
|
"/shutdown",
|
||||||
|
post(
|
||||||
|
|headers: axum::http::HeaderMap, State(state): State<Arc<AppState>>| async move {
|
||||||
|
let token_path = state.handler.state.base_dir.join("admin.token");
|
||||||
|
let expected_token = tokio::fs::read_to_string(&token_path)
|
||||||
|
.await
|
||||||
|
.unwrap_or_default()
|
||||||
|
.trim()
|
||||||
|
.to_string();
|
||||||
|
|
||||||
|
let auth_header = headers
|
||||||
|
.get(axum::http::header::AUTHORIZATION)
|
||||||
|
.and_then(|h| h.to_str().ok())
|
||||||
|
.unwrap_or_default();
|
||||||
|
|
||||||
|
if expected_token.is_empty() || auth_header != format!("Bearer {}", expected_token) {
|
||||||
|
return (axum::http::StatusCode::UNAUTHORIZED, "Unauthorized").into_response();
|
||||||
|
}
|
||||||
|
|
||||||
|
std::thread::spawn(|| {
|
||||||
|
tracing::info!(
|
||||||
|
"Received shutdown request via /shutdown endpoint. Exiting process cleanly."
|
||||||
|
);
|
||||||
|
std::thread::sleep(std::time::Duration::from_millis(100));
|
||||||
|
std::process::exit(0);
|
||||||
|
});
|
||||||
|
(axum::http::StatusCode::OK, "Shutting down...").into_response()
|
||||||
|
},
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.route(
|
||||||
|
"/",
|
||||||
|
get(|| async move { axum::response::Html(include_str!("dashboard.html")) }),
|
||||||
|
)
|
||||||
|
.route(
|
||||||
|
"/api/graph",
|
||||||
|
get({
|
||||||
|
let state_clone = app_state.handler.state.clone();
|
||||||
|
move || async move {
|
||||||
|
let graph_json = state_clone.read_graph(|g| serde_json::to_string(g).unwrap_or_else(|_| "{}".to_string()));
|
||||||
|
([(axum::http::header::CONTENT_TYPE, "application/json")], graph_json)
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.route(
|
||||||
|
"/api/tasks/{id}/complete",
|
||||||
|
post({
|
||||||
|
let state_clone = app_state.handler.state.clone();
|
||||||
|
move |axum::extract::Path(id): axum::extract::Path<String>| async move {
|
||||||
|
state_clone.tasks.modify(|tasks| {
|
||||||
|
for t in tasks.iter_mut() {
|
||||||
|
if t.id == id {
|
||||||
|
t.status = "completed".to_string();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
axum::Json(serde_json::json!({"status": "success"}))
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.route(
|
||||||
|
"/api/tasks",
|
||||||
|
get({
|
||||||
|
let state_clone = app_state.handler.state.clone();
|
||||||
|
move || async move {
|
||||||
|
let tasks_json = state_clone.tasks.read_with(|t| serde_json::to_string(t).unwrap_or_else(|_| "[]".to_string()));
|
||||||
|
([(axum::http::header::CONTENT_TYPE, "application/json")], tasks_json)
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.route(
|
||||||
|
"/api/sticky",
|
||||||
|
get({
|
||||||
|
let state_clone = app_state.handler.state.clone();
|
||||||
|
move || async move {
|
||||||
|
let sticky_json = state_clone.sticky.read_with(|s| serde_json::to_string(s).unwrap_or_else(|_| "[]".to_string()));
|
||||||
|
([(axum::http::header::CONTENT_TYPE, "application/json")], sticky_json)
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.route(
|
||||||
|
"/api/search",
|
||||||
|
get({
|
||||||
|
let state_clone = app_state.handler.state.clone();
|
||||||
|
move |axum::extract::Query(params): axum::extract::Query<
|
||||||
|
std::collections::HashMap<String, String>,
|
||||||
|
>| async move {
|
||||||
|
if let Some(q) = params.get("q")
|
||||||
|
&& let Ok(idx) = state_clone.search_index.read()
|
||||||
|
&& let Ok(results) = idx.search(q, None) {
|
||||||
|
let mut formatted_results = Vec::new();
|
||||||
|
for (id, doc_type, title, body, score) in results {
|
||||||
|
formatted_results.push(serde_json::json!({
|
||||||
|
"id": id,
|
||||||
|
"type_name": doc_type,
|
||||||
|
"title": title,
|
||||||
|
"content": body,
|
||||||
|
"score": score
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
return axum::Json(
|
||||||
|
serde_json::json!({ "results": formatted_results }),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
axum::Json(serde_json::json!({ "results": [] }))
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.route(
|
||||||
|
"/api/activity",
|
||||||
|
get({
|
||||||
|
let state_clone = app_state.handler.state.clone();
|
||||||
|
move || async move {
|
||||||
|
let activities_json = state_clone.recent_activities.read_with(|a| serde_json::to_string(a).unwrap_or_else(|_| "[]".to_string()));
|
||||||
|
([(axum::http::header::CONTENT_TYPE, "application/json")], activities_json)
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.route(
|
||||||
|
"/api/stats",
|
||||||
|
get({
|
||||||
|
let state_clone = app_state.handler.state.clone();
|
||||||
|
move || async move {
|
||||||
|
let (entities, relations) = state_clone.read_graph(|g| (g.entities.len(), g.relations.len()));
|
||||||
|
let tasks = state_clone.tasks.read_with(|items| items.len());
|
||||||
|
let snippets = state_clone.snippets.read_with(|items| items.len());
|
||||||
|
let tech_debts = state_clone.tech_debts.read_with(|items| items.len());
|
||||||
|
let adrs = state_clone.adrs.read_with(|items| items.len());
|
||||||
|
|
||||||
|
let ledger = state_clone.ledger.read_with(|items| items.len());
|
||||||
|
let sticky = state_clone.sticky.read_with(|items| items.len());
|
||||||
|
let error_fixes = state_clone.error_fixes.read_with(|items| items.len());
|
||||||
|
let pinned_files = state_clone.pinned_files.read_with(|items| items.len());
|
||||||
|
let session_summaries = state_clone.session_summaries.read_with(|items| items.len());
|
||||||
|
let handoff_memos = state_clone.handoff_memos.read_with(|items| items.len());
|
||||||
|
let env_fingerprints = state_clone.env_fingerprints.read_with(|items| items.len());
|
||||||
|
let env_requirements = state_clone.env_requirements.read_with(|items| items.len());
|
||||||
|
let milestones = state_clone.milestones.read_with(|items| items.len());
|
||||||
|
let environments = state_clone.environments.read_with(|items| items.len());
|
||||||
|
let pr_checklists = state_clone.pr_checklists.read_with(|items| items.len());
|
||||||
|
let gates = state_clone.gates.read_with(|items| items.len());
|
||||||
|
let context_workspaces = state_clone.context_workspaces.read_with(|items| items.len());
|
||||||
|
|
||||||
|
axum::Json(serde_json::json!({
|
||||||
|
"entities": entities,
|
||||||
|
"relations": relations,
|
||||||
|
"tasks": tasks,
|
||||||
|
"snippets": snippets,
|
||||||
|
"tech_debts": tech_debts,
|
||||||
|
"adrs": adrs,
|
||||||
|
"ledger": ledger,
|
||||||
|
"sticky": sticky,
|
||||||
|
"error_fixes": error_fixes,
|
||||||
|
"pinned_files": pinned_files,
|
||||||
|
"session_summaries": session_summaries,
|
||||||
|
"handoff_memos": handoff_memos,
|
||||||
|
"env_fingerprints": env_fingerprints,
|
||||||
|
"env_requirements": env_requirements,
|
||||||
|
"milestones": milestones,
|
||||||
|
"environments": environments,
|
||||||
|
"pr_checklists": pr_checklists,
|
||||||
|
"gates": gates,
|
||||||
|
"context_workspaces": context_workspaces
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.with_state(app_state);
|
||||||
|
|
||||||
|
tracing::info!("MCP Memory Server running on http://127.0.0.1:3000/sse");
|
||||||
|
let addr = std::env::var("MCP_PORT").unwrap_or_else(|_| "3000".to_string());
|
||||||
|
let addr: std::net::SocketAddr = format!("127.0.0.1:{}", addr)
|
||||||
|
.parse()
|
||||||
|
.expect("Invalid bind address");
|
||||||
|
|
||||||
|
let listener = match tokio::net::TcpListener::bind(&addr).await {
|
||||||
|
Ok(l) => l,
|
||||||
|
Err(e) => {
|
||||||
|
let log_path = dirs::home_dir()
|
||||||
|
.unwrap_or_default()
|
||||||
|
.join(".gemini/mcp_memory/daemon_error.log");
|
||||||
|
let _ =
|
||||||
|
tokio::fs::write(&log_path, format!("Failed to bind to {}: {}\n", addr, e)).await;
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
};
|
||||||
|
if let Err(e) = axum::serve(listener, app.into_make_service()).await {
|
||||||
|
let log_path = dirs::home_dir()
|
||||||
|
.unwrap_or_default()
|
||||||
|
.join(".gemini/mcp_memory/daemon_error.log");
|
||||||
|
let _ = tokio::fs::write(&log_path, format!("Server crashed: {}\n", e)).await;
|
||||||
|
}
|
||||||
@@ -0,0 +1,199 @@
|
|||||||
|
use crate::AppState;
|
||||||
|
use crate::api::rest::{gate_set_handler, gate_verify_handler, health_handler};
|
||||||
|
use crate::api::telemetry::nvim_telemetry_handler;
|
||||||
|
use crate::api::ws::ws_handler;
|
||||||
|
use axum::{
|
||||||
|
Router,
|
||||||
|
extract::State,
|
||||||
|
response::IntoResponse,
|
||||||
|
routing::{get, post},
|
||||||
|
};
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
pub fn create_router(app_state: Arc<AppState>) -> Router {
|
||||||
|
Router::new()
|
||||||
|
.route(
|
||||||
|
"/api/version",
|
||||||
|
get(|| async move {
|
||||||
|
axum::Json(serde_json::json!({
|
||||||
|
"version": env!("APP_VERSION"),
|
||||||
|
"git_hash": option_env!("GIT_HASH").unwrap_or("unknown")
|
||||||
|
}))
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.route("/ws", get(ws_handler))
|
||||||
|
.route("/health", get(health_handler))
|
||||||
|
.route("/nvim/telemetry", post(nvim_telemetry_handler))
|
||||||
|
.route("/gate/verify", get(gate_verify_handler))
|
||||||
|
.route("/gate/set", post(gate_set_handler))
|
||||||
|
.route(
|
||||||
|
"/shutdown",
|
||||||
|
post(
|
||||||
|
|headers: axum::http::HeaderMap, State(state): State<Arc<AppState>>| async move {
|
||||||
|
let token_path = state.handler.state.base_dir.join("admin.token");
|
||||||
|
let expected_token = tokio::fs::read_to_string(&token_path)
|
||||||
|
.await
|
||||||
|
.unwrap_or_default()
|
||||||
|
.trim()
|
||||||
|
.to_string();
|
||||||
|
|
||||||
|
let auth_header = headers
|
||||||
|
.get(axum::http::header::AUTHORIZATION)
|
||||||
|
.and_then(|h| h.to_str().ok())
|
||||||
|
.unwrap_or_default();
|
||||||
|
|
||||||
|
if expected_token.is_empty() || auth_header != format!("Bearer {}", expected_token) {
|
||||||
|
return (axum::http::StatusCode::UNAUTHORIZED, "Unauthorized").into_response();
|
||||||
|
}
|
||||||
|
|
||||||
|
std::thread::spawn(|| {
|
||||||
|
tracing::info!(
|
||||||
|
"Received shutdown request via /shutdown endpoint. Exiting process cleanly."
|
||||||
|
);
|
||||||
|
std::thread::sleep(std::time::Duration::from_millis(100));
|
||||||
|
std::process::exit(0);
|
||||||
|
});
|
||||||
|
(axum::http::StatusCode::OK, "Shutting down...").into_response()
|
||||||
|
},
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.route(
|
||||||
|
"/",
|
||||||
|
get(|| async move { axum::response::Html(include_str!("../dashboard.html")) }),
|
||||||
|
)
|
||||||
|
.route(
|
||||||
|
"/api/graph",
|
||||||
|
get({
|
||||||
|
let state_clone = app_state.handler.state.clone();
|
||||||
|
move || async move {
|
||||||
|
let graph_json = state_clone.read_graph(|g| serde_json::to_string(g).unwrap_or_else(|_| "{}".to_string()));
|
||||||
|
([(axum::http::header::CONTENT_TYPE, "application/json")], graph_json)
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.route(
|
||||||
|
"/api/tasks/{id}/complete",
|
||||||
|
post({
|
||||||
|
let state_clone = app_state.handler.state.clone();
|
||||||
|
move |axum::extract::Path(id): axum::extract::Path<String>| async move {
|
||||||
|
state_clone.tasks.modify(|tasks| {
|
||||||
|
for t in tasks.iter_mut() {
|
||||||
|
if t.id == id {
|
||||||
|
t.status = "completed".to_string();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
axum::Json(serde_json::json!({"status": "success"}))
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.route(
|
||||||
|
"/api/tasks",
|
||||||
|
get({
|
||||||
|
let state_clone = app_state.handler.state.clone();
|
||||||
|
move || async move {
|
||||||
|
let tasks_json = state_clone.tasks.read_with(|t| serde_json::to_string(t).unwrap_or_else(|_| "[]".to_string()));
|
||||||
|
([(axum::http::header::CONTENT_TYPE, "application/json")], tasks_json)
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.route(
|
||||||
|
"/api/sticky",
|
||||||
|
get({
|
||||||
|
let state_clone = app_state.handler.state.clone();
|
||||||
|
move || async move {
|
||||||
|
let sticky_json = state_clone.sticky.read_with(|s| serde_json::to_string(s).unwrap_or_else(|_| "[]".to_string()));
|
||||||
|
([(axum::http::header::CONTENT_TYPE, "application/json")], sticky_json)
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.route(
|
||||||
|
"/api/search",
|
||||||
|
get({
|
||||||
|
let state_clone = app_state.handler.state.clone();
|
||||||
|
move |axum::extract::Query(params): axum::extract::Query<
|
||||||
|
std::collections::HashMap<String, String>,
|
||||||
|
>| async move {
|
||||||
|
if let Some(q) = params.get("q")
|
||||||
|
&& let Ok(idx) = state_clone.search_index.read()
|
||||||
|
&& let Ok(results) = idx.search(q, None) {
|
||||||
|
let mut formatted_results = Vec::new();
|
||||||
|
for (id, doc_type, title, body, score) in results {
|
||||||
|
formatted_results.push(serde_json::json!({
|
||||||
|
"id": id,
|
||||||
|
"type_name": doc_type,
|
||||||
|
"title": title,
|
||||||
|
"content": body,
|
||||||
|
"score": score
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
return axum::Json(
|
||||||
|
serde_json::json!({ "results": formatted_results }),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
axum::Json(serde_json::json!({ "results": [] }))
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.route(
|
||||||
|
"/api/activity",
|
||||||
|
get({
|
||||||
|
let state_clone = app_state.handler.state.clone();
|
||||||
|
move || async move {
|
||||||
|
let activities_json = state_clone.recent_activities.read_with(|a| serde_json::to_string(a).unwrap_or_else(|_| "[]".to_string()));
|
||||||
|
([(axum::http::header::CONTENT_TYPE, "application/json")], activities_json)
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.route(
|
||||||
|
"/api/stats",
|
||||||
|
get({
|
||||||
|
let state_clone = app_state.handler.state.clone();
|
||||||
|
move || async move {
|
||||||
|
let (entities, relations) = state_clone.read_graph(|g| (g.entities.len(), g.relations.len()));
|
||||||
|
let tasks = state_clone.tasks.read_with(|items| items.len());
|
||||||
|
let snippets = state_clone.snippets.read_with(|items| items.len());
|
||||||
|
let tech_debts = state_clone.tech_debts.read_with(|items| items.len());
|
||||||
|
let adrs = state_clone.adrs.read_with(|items| items.len());
|
||||||
|
|
||||||
|
let ledger = state_clone.ledger.read_with(|items| items.len());
|
||||||
|
let sticky = state_clone.sticky.read_with(|items| items.len());
|
||||||
|
let error_fixes = state_clone.error_fixes.read_with(|items| items.len());
|
||||||
|
let pinned_files = state_clone.pinned_files.read_with(|items| items.len());
|
||||||
|
let session_summaries = state_clone.session_summaries.read_with(|items| items.len());
|
||||||
|
let handoff_memos = state_clone.handoff_memos.read_with(|items| items.len());
|
||||||
|
let env_fingerprints = state_clone.env_fingerprints.read_with(|items| items.len());
|
||||||
|
let env_requirements = state_clone.env_requirements.read_with(|items| items.len());
|
||||||
|
let milestones = state_clone.milestones.read_with(|items| items.len());
|
||||||
|
let environments = state_clone.environments.read_with(|items| items.len());
|
||||||
|
let pr_checklists = state_clone.pr_checklists.read_with(|items| items.len());
|
||||||
|
let gates = state_clone.gates.read_with(|items| items.len());
|
||||||
|
let context_workspaces = state_clone.context_workspaces.read_with(|items| items.len());
|
||||||
|
|
||||||
|
axum::Json(serde_json::json!({
|
||||||
|
"entities": entities,
|
||||||
|
"relations": relations,
|
||||||
|
"tasks": tasks,
|
||||||
|
"snippets": snippets,
|
||||||
|
"tech_debts": tech_debts,
|
||||||
|
"adrs": adrs,
|
||||||
|
"ledger": ledger,
|
||||||
|
"sticky": sticky,
|
||||||
|
"error_fixes": error_fixes,
|
||||||
|
"pinned_files": pinned_files,
|
||||||
|
"session_summaries": session_summaries,
|
||||||
|
"handoff_memos": handoff_memos,
|
||||||
|
"env_fingerprints": env_fingerprints,
|
||||||
|
"env_requirements": env_requirements,
|
||||||
|
"milestones": milestones,
|
||||||
|
"environments": environments,
|
||||||
|
"pr_checklists": pr_checklists,
|
||||||
|
"gates": gates,
|
||||||
|
"context_workspaces": context_workspaces
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.with_state(app_state)
|
||||||
|
}
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
use crate::AppState;
|
||||||
|
use axum::extract::State;
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
#[derive(serde::Deserialize, serde::Serialize, Debug)]
|
||||||
|
pub struct NvimTelemetry {
|
||||||
|
pub session_id: String,
|
||||||
|
pub event: String,
|
||||||
|
pub file: Option<String>,
|
||||||
|
pub line: Option<i64>,
|
||||||
|
pub col: Option<i64>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn nvim_telemetry_handler(
|
||||||
|
State(state): State<Arc<AppState>>,
|
||||||
|
axum::Json(payload): axum::Json<NvimTelemetry>,
|
||||||
|
) -> impl axum::response::IntoResponse {
|
||||||
|
// 1. Update active_nvim.txt if FocusGained, BufEnter, or VimEnter
|
||||||
|
if payload.event == "FocusGained" || payload.event == "BufEnter" || payload.event == "VimEnter"
|
||||||
|
{
|
||||||
|
let profile =
|
||||||
|
std::env::var("USERPROFILE").unwrap_or_else(|_| "C:\\Users\\reazul.ashraf".into());
|
||||||
|
let win_path = format!("{}\\.gemini\\active_nvim.txt", profile);
|
||||||
|
let _ = tokio::fs::write(&win_path, &payload.session_id).await;
|
||||||
|
|
||||||
|
let wsl_path = "\\\\wsl.localhost\\Ubuntu\\home\\riz\\.gemini\\active_nvim.txt";
|
||||||
|
let _ = tokio::fs::write(wsl_path, &payload.session_id).await;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Broadcast to UI WebSockets
|
||||||
|
let ws_msg = serde_json::json!({
|
||||||
|
"type": "nvim_telemetry",
|
||||||
|
"data": payload
|
||||||
|
});
|
||||||
|
|
||||||
|
let msg_str = ws_msg.to_string();
|
||||||
|
let senders: Vec<_> = state
|
||||||
|
.clients
|
||||||
|
.read()
|
||||||
|
.unwrap_or_else(|e| e.into_inner())
|
||||||
|
.values()
|
||||||
|
.cloned()
|
||||||
|
.collect();
|
||||||
|
for tx in senders {
|
||||||
|
let _ = tx.try_send(msg_str.clone());
|
||||||
|
}
|
||||||
|
|
||||||
|
axum::Json(serde_json::json!({"status": "ok"}))
|
||||||
|
}
|
||||||
@@ -0,0 +1,144 @@
|
|||||||
|
use crate::AppState;
|
||||||
|
use axum::extract::{
|
||||||
|
Query, State,
|
||||||
|
ws::{Message, WebSocket},
|
||||||
|
};
|
||||||
|
use axum::response::IntoResponse;
|
||||||
|
use futures_util::{SinkExt, StreamExt};
|
||||||
|
use std::collections::HashMap;
|
||||||
|
use std::sync::Arc;
|
||||||
|
use std::sync::atomic::Ordering;
|
||||||
|
use tokio::sync::mpsc;
|
||||||
|
|
||||||
|
pub async fn ws_handler(
|
||||||
|
ws: axum::extract::ws::WebSocketUpgrade,
|
||||||
|
_headers: axum::http::HeaderMap,
|
||||||
|
State(state): State<Arc<AppState>>,
|
||||||
|
Query(query): Query<HashMap<String, String>>,
|
||||||
|
) -> axum::response::Response {
|
||||||
|
let client_type = query
|
||||||
|
.get("client")
|
||||||
|
.cloned()
|
||||||
|
.unwrap_or_else(|| "unknown".to_string());
|
||||||
|
ws.on_upgrade(move |socket| handle_socket(socket, state, client_type))
|
||||||
|
.into_response()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct SessionCleanup {
|
||||||
|
pub session_id: String,
|
||||||
|
pub state: Arc<AppState>,
|
||||||
|
pub send_task: tokio::task::JoinHandle<()>,
|
||||||
|
pub recv_task: tokio::task::JoinHandle<()>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Drop for SessionCleanup {
|
||||||
|
fn drop(&mut self) {
|
||||||
|
tracing::info!("Dropping session {}", self.session_id);
|
||||||
|
self.state
|
||||||
|
.clients
|
||||||
|
.write()
|
||||||
|
.unwrap_or_else(|e| e.into_inner())
|
||||||
|
.remove(&self.session_id);
|
||||||
|
self.send_task.abort();
|
||||||
|
self.recv_task.abort();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn handle_socket(socket: WebSocket, state: Arc<AppState>, _client_type: String) {
|
||||||
|
let session_id = format!("{}", state.next_id.fetch_add(1, Ordering::SeqCst));
|
||||||
|
let (tx, mut rx) = mpsc::channel::<String>(100);
|
||||||
|
|
||||||
|
state
|
||||||
|
.clients
|
||||||
|
.write()
|
||||||
|
.unwrap_or_else(|e| e.into_inner())
|
||||||
|
.insert(session_id.clone(), tx.clone());
|
||||||
|
|
||||||
|
let (mut sender, mut receiver) = socket.split();
|
||||||
|
|
||||||
|
let send_task = tokio::spawn(async move {
|
||||||
|
while let Some(msg) = rx.recv().await {
|
||||||
|
tracing::trace!(
|
||||||
|
"Sending message to websocket (length: {}): {}",
|
||||||
|
msg.len(),
|
||||||
|
msg
|
||||||
|
);
|
||||||
|
if sender.send(Message::Text(msg.into())).await.is_err() {
|
||||||
|
tracing::error!("Failed to send message to websocket");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
let handler = Arc::clone(&state.handler);
|
||||||
|
let state_clone = Arc::clone(&state);
|
||||||
|
let session_id_clone = session_id.clone();
|
||||||
|
|
||||||
|
let recv_task = tokio::spawn(async move {
|
||||||
|
while let Some(msg_result) = receiver.next().await {
|
||||||
|
match msg_result {
|
||||||
|
Ok(Message::Text(text)) => {
|
||||||
|
tracing::info!(
|
||||||
|
"Received text message from websocket (length: {})",
|
||||||
|
text.len()
|
||||||
|
);
|
||||||
|
tracing::trace!("Message content: {}", text);
|
||||||
|
if let Ok(payload) = serde_json::from_str::<serde_json::Value>(&text) {
|
||||||
|
// Process MCP request
|
||||||
|
if let Some(response) = handler.handle_request(payload).await {
|
||||||
|
let res_str = serde_json::to_string(&response).unwrap_or_default();
|
||||||
|
let tx_opt = state_clone
|
||||||
|
.clients
|
||||||
|
.read()
|
||||||
|
.unwrap_or_else(|e| e.into_inner())
|
||||||
|
.get(&session_id_clone)
|
||||||
|
.cloned();
|
||||||
|
if let Some(client_tx) = tx_opt {
|
||||||
|
if let Err(e) = client_tx.send(res_str).await {
|
||||||
|
tracing::error!(
|
||||||
|
"Failed to send response to client channel for session {}: {}",
|
||||||
|
session_id_clone,
|
||||||
|
e
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
tracing::warn!(
|
||||||
|
"Could not find client_tx for session_id {} when trying to send response",
|
||||||
|
session_id_clone
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
tracing::warn!(
|
||||||
|
"Failed to parse payload as JSON from websocket message: {}",
|
||||||
|
text
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(other) => {
|
||||||
|
tracing::info!("Received non-text message from websocket: {:?}", other);
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
tracing::error!("Websocket receive error: {}", e);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
let mut cleanup = SessionCleanup {
|
||||||
|
session_id: session_id.clone(),
|
||||||
|
state: Arc::clone(&state),
|
||||||
|
send_task,
|
||||||
|
recv_task,
|
||||||
|
};
|
||||||
|
|
||||||
|
tokio::select! {
|
||||||
|
_ = &mut cleanup.send_task => {
|
||||||
|
tracing::info!("Websocket send task finished for session {}", session_id);
|
||||||
|
},
|
||||||
|
_ = &mut cleanup.recv_task => {
|
||||||
|
tracing::info!("Websocket recv task finished for session {}", session_id);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -1,62 +0,0 @@
|
|||||||
import os
|
|
||||||
import re
|
|
||||||
|
|
||||||
def bake_nvim_safeguards():
|
|
||||||
filepath = 'nvim-core/src/lib.rs'
|
|
||||||
with open(filepath, 'r', encoding='utf-8') as f:
|
|
||||||
content = f.read()
|
|
||||||
|
|
||||||
# 1. We're going to inject an AST-level safeguard inside "nvim_execute_lua"
|
|
||||||
# Find the execute_lua block
|
|
||||||
target = """ "nvim_execute_lua" => {
|
|
||||||
if let Some(code) = args.get("code").and_then(|v| v.as_str()) {
|
|
||||||
match execute_nvim_lua(code).await {"""
|
|
||||||
|
|
||||||
safeguarded = """ "nvim_execute_lua" => {
|
|
||||||
if let Some(code) = args.get("code").and_then(|v| v.as_str()) {
|
|
||||||
// BAKE IN: Block interactive prompts that cause server deadlocks
|
|
||||||
let lower_code = code.to_lowercase();
|
|
||||||
if lower_code.contains("vim.fn.input") || lower_code.contains("vim.ui.select") || lower_code.contains("vim.fn.confirm") || lower_code.contains("vim.ui.input") {
|
|
||||||
send_error(id, -32600, "CRITICAL ERROR: Interactive Lua functions (input, select, confirm) are strictly forbidden via MCP as they will hang the headless server.").await;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
// Block the 'c' confirm flag in vim.cmd substitutions
|
|
||||||
if (code.contains("vim.cmd") || code.contains("vim.api.nvim_command")) && code.contains("%s") && code.contains("c") {
|
|
||||||
// A rough heuristic to block `%s/foo/bar/gc`
|
|
||||||
if regex::Regex::new(r"%s.*/.*c").unwrap().is_match(code) {
|
|
||||||
send_error(id, -32600, "CRITICAL ERROR: The 'c' (confirm) flag in Neovim substitutions is strictly forbidden via MCP as it triggers an interactive prompt that hangs the headless server. Use '/ge' instead.").await;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
match execute_nvim_lua(code).await {"""
|
|
||||||
|
|
||||||
if target in content:
|
|
||||||
# Also need to add regex as a dependency or just use basic string matching.
|
|
||||||
# Let's just use basic string matching for the %s block to avoid adding the regex crate to nvim-core if not present
|
|
||||||
|
|
||||||
safeguarded_no_regex = """ "nvim_execute_lua" => {
|
|
||||||
if let Some(code) = args.get("code").and_then(|v| v.as_str()) {
|
|
||||||
// BAKE IN: Block interactive prompts that cause server deadlocks
|
|
||||||
let lower_code = code.to_lowercase();
|
|
||||||
if lower_code.contains("vim.fn.input") || lower_code.contains("vim.ui.select") || lower_code.contains("vim.fn.confirm") || lower_code.contains("vim.ui.input") {
|
|
||||||
send_error(id, -32600, "CRITICAL ERROR: Interactive Lua functions (input, select, confirm) are strictly forbidden via MCP as they will hang the headless server.").await;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
// Block the 'c' confirm flag in vim.cmd substitutions
|
|
||||||
if (code.contains("vim.cmd") || code.contains("vim.api.nvim_command")) && code.contains("%s") && (code.contains("gc'") || code.contains('gc"') || code.contains("gc\\n") || code.contains("c'") || code.contains('c"')) {
|
|
||||||
send_error(id, -32600, "CRITICAL ERROR: The 'c' (confirm) flag in Neovim substitutions is strictly forbidden via MCP as it triggers an interactive prompt that hangs the headless server. Use '/g' or '/ge' instead.").await;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
match execute_nvim_lua(code).await {"""
|
|
||||||
|
|
||||||
content = content.replace(target, safeguarded_no_regex)
|
|
||||||
print("Baked safeguards into nvim_execute_lua")
|
|
||||||
|
|
||||||
with open(filepath, 'w', encoding='utf-8') as f:
|
|
||||||
f.write(content)
|
|
||||||
else:
|
|
||||||
print("Could not find nvim_execute_lua block")
|
|
||||||
|
|
||||||
bake_nvim_safeguards()
|
|
||||||
@@ -1,150 +0,0 @@
|
|||||||
import os
|
|
||||||
|
|
||||||
def rewrite_graph():
|
|
||||||
with open('server/src/handlers_v2/graph.rs', 'r', encoding='utf-8') as f:
|
|
||||||
content = f.read()
|
|
||||||
|
|
||||||
old_block = """ let mut included: HashSet<&str> = HashSet::new();
|
|
||||||
let mut to_draw = Vec::new();
|
|
||||||
|
|
||||||
state.read_graph(|full| {
|
|
||||||
for (name, e) in &full.entities {
|
|
||||||
if let Some(ns) = &req.namespace
|
|
||||||
&& e.namespace != *ns
|
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if query.is_empty()
|
|
||||||
|| contains_ignore_ascii_case(name, &query)
|
|
||||||
|| contains_ignore_ascii_case(&e.entity_type, &query)
|
|
||||||
{
|
|
||||||
included.insert(name.as_str());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for r in &full.relations {
|
|
||||||
if let Some(ns) = &req.namespace
|
|
||||||
&& r.namespace != *ns
|
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if query.is_empty() || included.contains(r.from.as_str()) || included.contains(r.to.as_str()) {
|
|
||||||
included.insert(r.from.as_str());
|
|
||||||
included.insert(r.to.as_str());
|
|
||||||
to_draw.push(r.clone());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
use std::fmt::Write;
|
|
||||||
let mut output = String::with_capacity(included.len() * 40 + to_draw.len() * 60);
|
|
||||||
output.push_str("graph TD;\\n");
|
|
||||||
|
|
||||||
let sanitize = |s: &str, id_mode: bool| -> String {
|
|
||||||
let mut out = String::with_capacity(s.len());
|
|
||||||
for c in s.chars() {
|
|
||||||
if c != '"' && c != '(' && c != ')' {
|
|
||||||
if id_mode && (c == ' ' || c == '-' || c == '.') {
|
|
||||||
out.push('_');
|
|
||||||
} else {
|
|
||||||
out.push(c);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
out
|
|
||||||
};
|
|
||||||
|
|
||||||
for name in &included {
|
|
||||||
let _ = writeln!(
|
|
||||||
output,
|
|
||||||
" id_{}[\\"{}\\"];",
|
|
||||||
sanitize(name, true),
|
|
||||||
sanitize(name, false)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
for r in to_draw {
|
|
||||||
let _ = writeln!(
|
|
||||||
output,
|
|
||||||
" id_{}-->|\\"{}\\"|id_{};",
|
|
||||||
sanitize(&r.from, true),
|
|
||||||
r.relation_type.replace("\\"", ""),
|
|
||||||
sanitize(&r.to, true)
|
|
||||||
);
|
|
||||||
}"""
|
|
||||||
|
|
||||||
new_block = """ use std::fmt::Write;
|
|
||||||
let mut output = state.read_graph(|full| {
|
|
||||||
let mut included: HashSet<&str> = HashSet::new();
|
|
||||||
let mut to_draw = Vec::new();
|
|
||||||
|
|
||||||
for (name, e) in &full.entities {
|
|
||||||
if let Some(ns) = &req.namespace
|
|
||||||
&& e.namespace != *ns
|
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if query.is_empty()
|
|
||||||
|| contains_ignore_ascii_case(name, &query)
|
|
||||||
|| contains_ignore_ascii_case(&e.entity_type, &query)
|
|
||||||
{
|
|
||||||
included.insert(name.as_str());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for r in &full.relations {
|
|
||||||
if let Some(ns) = &req.namespace
|
|
||||||
&& r.namespace != *ns
|
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if query.is_empty() || included.contains(r.from.as_str()) || included.contains(r.to.as_str()) {
|
|
||||||
included.insert(r.from.as_str());
|
|
||||||
included.insert(r.to.as_str());
|
|
||||||
to_draw.push(r.clone());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut out = String::with_capacity(included.len() * 40 + to_draw.len() * 60);
|
|
||||||
out.push_str("graph TD;\\n");
|
|
||||||
|
|
||||||
let sanitize = |s: &str, id_mode: bool| -> String {
|
|
||||||
let mut o = String::with_capacity(s.len());
|
|
||||||
for c in s.chars() {
|
|
||||||
if c != '"' && c != '(' && c != ')' {
|
|
||||||
if id_mode && (c == ' ' || c == '-' || c == '.') {
|
|
||||||
o.push('_');
|
|
||||||
} else {
|
|
||||||
o.push(c);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
o
|
|
||||||
};
|
|
||||||
|
|
||||||
for name in &included {
|
|
||||||
let _ = writeln!(
|
|
||||||
out,
|
|
||||||
" id_{}[\\"{}\\"];",
|
|
||||||
sanitize(name, true),
|
|
||||||
sanitize(name, false)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
for r in to_draw {
|
|
||||||
let _ = writeln!(
|
|
||||||
out,
|
|
||||||
" id_{}-->|\\"{}\\"|id_{};",
|
|
||||||
sanitize(&r.from, true),
|
|
||||||
r.relation_type.replace("\\"", ""),
|
|
||||||
sanitize(&r.to, true)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
out
|
|
||||||
});"""
|
|
||||||
|
|
||||||
if old_block in content:
|
|
||||||
with open('server/src/handlers_v2/graph.rs', 'w', encoding='utf-8') as f:
|
|
||||||
f.write(content.replace(old_block, new_block))
|
|
||||||
print("Replaced visualize_graph")
|
|
||||||
else:
|
|
||||||
print("Could not find old block")
|
|
||||||
|
|
||||||
rewrite_graph()
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
use rmcp::model::{InitializeResult, ServerCapabilities};
|
|
||||||
|
|
||||||
fn main() {
|
|
||||||
let init = InitializeResult::new(ServerCapabilities::builder().enable_tools().build())
|
|
||||||
.with_server_info(rmcp::model::Implementation::new(
|
|
||||||
"gemini-mcp-memory",
|
|
||||||
"3.0.0",
|
|
||||||
));
|
|
||||||
println!("{}", serde_json::to_string_pretty(&init).unwrap());
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,64 @@
|
|||||||
|
use std::path::Path;
|
||||||
|
use std::sync::Arc;
|
||||||
|
use redb::{Database, ReadableTable};
|
||||||
|
use crate::store::STORE_TABLE;
|
||||||
|
|
||||||
|
pub fn init_redb(base: &Path) -> Arc<Database> {
|
||||||
|
let redb_path = base.join("mcp_store.redb");
|
||||||
|
let db = Arc::new(redb::Database::create(&redb_path).expect("Failed to create redb database"));
|
||||||
|
|
||||||
|
// Ensure the table exists and migrate legacy JSON files
|
||||||
|
let write_txn = db.begin_write().expect("Failed to begin write txn on redb");
|
||||||
|
{
|
||||||
|
let mut table = write_txn
|
||||||
|
.open_table(STORE_TABLE)
|
||||||
|
.expect("Failed to open STORE_TABLE");
|
||||||
|
|
||||||
|
let stores = vec![
|
||||||
|
("knowledge_graph_master", "knowledge_graph_master.json"),
|
||||||
|
("audit_ledger", "audit_ledger.json"),
|
||||||
|
("sticky_notes", "sticky_notes.json"),
|
||||||
|
("tasks", "tasks.json"),
|
||||||
|
("snippets", "snippets.json"),
|
||||||
|
("adrs", "adrs.json"),
|
||||||
|
("preferences", "preferences.json"),
|
||||||
|
("error_fixes", "error_fixes.json"),
|
||||||
|
("pinned_files", "pinned_files.json"),
|
||||||
|
("session_summaries", "session_summaries.json"),
|
||||||
|
("handoff_memos", "handoff_memos.json"),
|
||||||
|
("env_fingerprints", "env_fingerprints.json"),
|
||||||
|
("env_requirements", "env_requirements.json"),
|
||||||
|
("milestones", "milestones.json"),
|
||||||
|
("environments", "environments.json"),
|
||||||
|
("pr_checklists", "pr_checklists.json"),
|
||||||
|
("tech_debts", "tech_debts.json"),
|
||||||
|
("gates", "gates.json"),
|
||||||
|
("context_workspaces", "context_workspaces.json"),
|
||||||
|
];
|
||||||
|
|
||||||
|
for (key, file_name) in stores.iter() {
|
||||||
|
if table
|
||||||
|
.get(*key)
|
||||||
|
.expect("Failed to read from table")
|
||||||
|
.is_none()
|
||||||
|
{
|
||||||
|
let json_path = base.join(file_name);
|
||||||
|
if json_path.exists()
|
||||||
|
&& let Ok(data) = std::fs::read(&json_path)
|
||||||
|
&& serde_json::from_slice::<serde_json::Value>(&data).is_ok()
|
||||||
|
{
|
||||||
|
table
|
||||||
|
.insert(*key, data.as_slice())
|
||||||
|
.expect("Failed to insert migrated data");
|
||||||
|
let _ = std::fs::rename(
|
||||||
|
&json_path,
|
||||||
|
json_path.with_extension("json.migrated"),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
write_txn.commit().expect("Failed to commit db migration");
|
||||||
|
|
||||||
|
db
|
||||||
|
}
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
use axum::{
|
||||||
|
http::StatusCode,
|
||||||
|
response::{IntoResponse, Response},
|
||||||
|
Json,
|
||||||
|
};
|
||||||
|
use serde_json::json;
|
||||||
|
use thiserror::Error;
|
||||||
|
|
||||||
|
#[derive(Error, Debug)]
|
||||||
|
pub enum AppError {
|
||||||
|
#[error("Not Found: {0}")]
|
||||||
|
NotFound(String),
|
||||||
|
|
||||||
|
#[error("Forbidden: {0}")]
|
||||||
|
Forbidden(String),
|
||||||
|
|
||||||
|
#[error("Internal Server Error: {0}")]
|
||||||
|
Internal(String),
|
||||||
|
|
||||||
|
#[error("Bad Request: {0}")]
|
||||||
|
BadRequest(String),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl IntoResponse for AppError {
|
||||||
|
fn into_response(self) -> Response {
|
||||||
|
let (status, error_message) = match &self {
|
||||||
|
AppError::NotFound(msg) => (StatusCode::NOT_FOUND, msg.clone()),
|
||||||
|
AppError::Forbidden(msg) => (StatusCode::FORBIDDEN, msg.clone()),
|
||||||
|
AppError::Internal(msg) => (StatusCode::INTERNAL_SERVER_ERROR, msg.clone()),
|
||||||
|
AppError::BadRequest(msg) => (StatusCode::BAD_REQUEST, msg.clone()),
|
||||||
|
};
|
||||||
|
|
||||||
|
let body = Json(json!({
|
||||||
|
"error": error_message,
|
||||||
|
}));
|
||||||
|
|
||||||
|
(status, body).into_response()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,61 +0,0 @@
|
|||||||
import os
|
|
||||||
|
|
||||||
def fix_env_and_workspaces():
|
|
||||||
filepath = 'server/src/handlers_v2/env.rs'
|
|
||||||
with open(filepath, 'r', encoding='utf-8') as f:
|
|
||||||
content = f.read()
|
|
||||||
|
|
||||||
old_env = """ let fps = state.env_fingerprints.read();
|
|
||||||
if let Some(fp) = fps.get(&req.namespace) {"""
|
|
||||||
|
|
||||||
new_env = """ let data = state.env_fingerprints.read_with(|fps| {
|
|
||||||
fps.get(&req.namespace).cloned()
|
|
||||||
});
|
|
||||||
if let Some(fp) = data {"""
|
|
||||||
|
|
||||||
if old_env in content:
|
|
||||||
content = content.replace(old_env, new_env)
|
|
||||||
print('Replaced env read')
|
|
||||||
else:
|
|
||||||
print('Could not find old_env')
|
|
||||||
|
|
||||||
with open(filepath, 'w', encoding='utf-8') as f:
|
|
||||||
f.write(content)
|
|
||||||
|
|
||||||
filepath = 'server/src/handlers_v2/workspaces.rs'
|
|
||||||
with open(filepath, 'r', encoding='utf-8') as f:
|
|
||||||
content = f.read()
|
|
||||||
|
|
||||||
old_ws = """ let query = req.query.to_lowercase();
|
|
||||||
let snippets = state.snippets.read();
|
|
||||||
let mut results = Vec::new();
|
|
||||||
for s in snippets {
|
|
||||||
if contains_ignore_ascii_case(&s.name, &query)
|
|
||||||
|| contains_ignore_ascii_case(&s.description, &query)
|
|
||||||
|| contains_ignore_ascii_case(&s.language, &query)
|
|
||||||
{
|
|
||||||
results.push(s);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
let data = serde_json::to_string(&results).unwrap_or_default();"""
|
|
||||||
|
|
||||||
new_ws = """ let query = req.query.to_lowercase();
|
|
||||||
let data = state.snippets.read_with(|snippets| {
|
|
||||||
let results: Vec<_> = snippets.iter().filter(|s| {
|
|
||||||
contains_ignore_ascii_case(&s.name, &query)
|
|
||||||
|| contains_ignore_ascii_case(&s.description, &query)
|
|
||||||
|| contains_ignore_ascii_case(&s.language, &query)
|
|
||||||
}).cloned().collect();
|
|
||||||
serde_json::to_string(&results).unwrap_or_default()
|
|
||||||
});"""
|
|
||||||
|
|
||||||
if old_ws in content:
|
|
||||||
content = content.replace(old_ws, new_ws)
|
|
||||||
print('Replaced workspaces read')
|
|
||||||
else:
|
|
||||||
print('Could not find old_ws')
|
|
||||||
|
|
||||||
with open(filepath, 'w', encoding='utf-8') as f:
|
|
||||||
f.write(content)
|
|
||||||
|
|
||||||
fix_env_and_workspaces()
|
|
||||||
@@ -1,60 +0,0 @@
|
|||||||
import os
|
|
||||||
|
|
||||||
def fix_main_stats():
|
|
||||||
filepath = 'server/src/main.rs'
|
|
||||||
with open(filepath, 'r', encoding='utf-8') as f:
|
|
||||||
content = f.read()
|
|
||||||
|
|
||||||
old_stats = """ 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();
|
|
||||||
|
|
||||||
let ledger = state_clone.ledger.read().len();
|
|
||||||
let sticky = state_clone.sticky.read().len();
|
|
||||||
let error_fixes = state_clone.error_fixes.read().len();
|
|
||||||
let pinned_files = state_clone.pinned_files.read().len();
|
|
||||||
let session_summaries = state_clone.session_summaries.read().len();
|
|
||||||
let handoff_memos = state_clone.handoff_memos.read().len();
|
|
||||||
let env_fingerprints = state_clone.env_fingerprints.read().len();
|
|
||||||
let env_requirements = state_clone.env_requirements.read().len();
|
|
||||||
let milestones = state_clone.milestones.read().len();
|
|
||||||
let environments = state_clone.environments.read().len();
|
|
||||||
let pr_checklists = state_clone.pr_checklists.read().len();
|
|
||||||
let gates = state_clone.gates.read().len();
|
|
||||||
let context_workspaces = state_clone.context_workspaces.read().len();"""
|
|
||||||
|
|
||||||
new_stats = """ let (entities, relations) = state_clone.read_graph(|g| (g.entities.len(), g.relations.len()));
|
|
||||||
let tasks = state_clone.tasks.read_with(|items| items.len());
|
|
||||||
let snippets = state_clone.snippets.read_with(|items| items.len());
|
|
||||||
let tech_debts = state_clone.tech_debts.read_with(|items| items.len());
|
|
||||||
let adrs = state_clone.adrs.read_with(|items| items.len());
|
|
||||||
|
|
||||||
let ledger = state_clone.ledger.read_with(|items| items.len());
|
|
||||||
let sticky = state_clone.sticky.read_with(|items| items.len());
|
|
||||||
let error_fixes = state_clone.error_fixes.read_with(|items| items.len());
|
|
||||||
let pinned_files = state_clone.pinned_files.read_with(|items| items.len());
|
|
||||||
let session_summaries = state_clone.session_summaries.read_with(|items| items.len());
|
|
||||||
let handoff_memos = state_clone.handoff_memos.read_with(|items| items.len());
|
|
||||||
let env_fingerprints = state_clone.env_fingerprints.read_with(|items| items.len());
|
|
||||||
let env_requirements = state_clone.env_requirements.read_with(|items| items.len());
|
|
||||||
let milestones = state_clone.milestones.read_with(|items| items.len());
|
|
||||||
let environments = state_clone.environments.read_with(|items| items.len());
|
|
||||||
let pr_checklists = state_clone.pr_checklists.read_with(|items| items.len());
|
|
||||||
let gates = state_clone.gates.read_with(|items| items.len());
|
|
||||||
let context_workspaces = state_clone.context_workspaces.read_with(|items| items.len());"""
|
|
||||||
|
|
||||||
if old_stats in content:
|
|
||||||
content = content.replace(old_stats, new_stats)
|
|
||||||
print('Replaced stats reads')
|
|
||||||
else:
|
|
||||||
print('Could not find old_stats')
|
|
||||||
|
|
||||||
with open(filepath, 'w', encoding='utf-8') as f:
|
|
||||||
f.write(content)
|
|
||||||
|
|
||||||
fix_main_stats()
|
|
||||||
@@ -1,111 +0,0 @@
|
|||||||
import os
|
|
||||||
|
|
||||||
def fix_meta():
|
|
||||||
filepath = 'server/src/handlers_v2/meta.rs'
|
|
||||||
with open(filepath, 'r', encoding='utf-8') as f:
|
|
||||||
content = f.read()
|
|
||||||
|
|
||||||
old_omni = """ let q = req.query.to_lowercase();
|
|
||||||
let tech_debts: Vec<_> = state
|
|
||||||
.tech_debts
|
|
||||||
.read()
|
|
||||||
.into_iter()
|
|
||||||
.filter(|d| {
|
|
||||||
req.namespace.as_ref().is_none_or(|ns| d.namespace == *ns)
|
|
||||||
&& (contains_ignore_ascii_case(&d.description, &q)
|
|
||||||
|| contains_ignore_ascii_case(&d.ideal_solution, &q))
|
|
||||||
})
|
|
||||||
.collect();
|
|
||||||
let memos: Vec<_> = state
|
|
||||||
.handoff_memos
|
|
||||||
.read()
|
|
||||||
.into_iter()
|
|
||||||
.filter(|m| {
|
|
||||||
req.namespace.as_ref().is_none_or(|ns| m.namespace == *ns)
|
|
||||||
&& contains_ignore_ascii_case(&m.content, &q)
|
|
||||||
})
|
|
||||||
.collect();
|
|
||||||
let error_fixes: Vec<_> = state
|
|
||||||
.error_fixes
|
|
||||||
.read()
|
|
||||||
.into_iter()
|
|
||||||
.filter(|f| {
|
|
||||||
contains_ignore_ascii_case(&f.signature, &q)
|
|
||||||
|| contains_ignore_ascii_case(&f.solution, &q)
|
|
||||||
})
|
|
||||||
.collect();"""
|
|
||||||
|
|
||||||
new_omni = """ let q = req.query.to_lowercase();
|
|
||||||
let tech_debts = state.tech_debts.read_with(|debts| {
|
|
||||||
debts.iter().filter(|d| {
|
|
||||||
req.namespace.as_ref().is_none_or(|ns| d.namespace == *ns)
|
|
||||||
&& (contains_ignore_ascii_case(&d.description, &q)
|
|
||||||
|| contains_ignore_ascii_case(&d.ideal_solution, &q))
|
|
||||||
}).cloned().collect::<Vec<_>>()
|
|
||||||
});
|
|
||||||
let memos = state.handoff_memos.read_with(|memos| {
|
|
||||||
memos.iter().filter(|m| {
|
|
||||||
req.namespace.as_ref().is_none_or(|ns| m.namespace == *ns)
|
|
||||||
&& contains_ignore_ascii_case(&m.content, &q)
|
|
||||||
}).cloned().collect::<Vec<_>>()
|
|
||||||
});
|
|
||||||
let error_fixes = state.error_fixes.read_with(|fixes| {
|
|
||||||
fixes.iter().filter(|f| {
|
|
||||||
contains_ignore_ascii_case(&f.signature, &q)
|
|
||||||
|| contains_ignore_ascii_case(&f.solution, &q)
|
|
||||||
}).cloned().collect::<Vec<_>>()
|
|
||||||
});"""
|
|
||||||
|
|
||||||
if old_omni in content:
|
|
||||||
content = content.replace(old_omni, new_omni)
|
|
||||||
print('Replaced omni_search vecs')
|
|
||||||
else:
|
|
||||||
print('Could not find old_omni')
|
|
||||||
|
|
||||||
old_health = """ let active_tasks = state
|
|
||||||
.tasks
|
|
||||||
.read()
|
|
||||||
.into_iter()
|
|
||||||
.filter(|t| t.status != "done")
|
|
||||||
.count();
|
|
||||||
let unresolved_debt = state
|
|
||||||
.tech_debts
|
|
||||||
.read()
|
|
||||||
.into_iter()
|
|
||||||
.filter(|d| d.namespace == req.namespace && !d.is_resolved)
|
|
||||||
.count();
|
|
||||||
let unread_memos = state
|
|
||||||
.handoff_memos
|
|
||||||
.read()
|
|
||||||
.into_iter()
|
|
||||||
.filter(|m| m.namespace == req.namespace)
|
|
||||||
.count();
|
|
||||||
let active_milestones = state
|
|
||||||
.milestones
|
|
||||||
.read()
|
|
||||||
.into_iter()
|
|
||||||
.filter(|m| m.namespace == req.namespace && m.status != "done")
|
|
||||||
.count();
|
|
||||||
let remaining_checklists = state
|
|
||||||
.pr_checklists
|
|
||||||
.read()
|
|
||||||
.into_iter()
|
|
||||||
.filter(|c| c.namespace == req.namespace)
|
|
||||||
.count();"""
|
|
||||||
|
|
||||||
new_health = """ let active_tasks = state.tasks.read_with(|tasks| tasks.iter().filter(|t| t.status != "done").count());
|
|
||||||
let unresolved_debt = state.tech_debts.read_with(|debts| debts.iter().filter(|d| d.namespace == req.namespace && !d.is_resolved).count());
|
|
||||||
let unread_memos = state.handoff_memos.read_with(|memos| memos.iter().filter(|m| m.namespace == req.namespace).count());
|
|
||||||
let active_milestones = state.milestones.read_with(|milestones| milestones.iter().filter(|m| m.namespace == req.namespace && m.status != "done").count());
|
|
||||||
let remaining_checklists = state.pr_checklists.read_with(|checklists| checklists.iter().filter(|c| c.namespace == req.namespace).count());"""
|
|
||||||
|
|
||||||
if old_health in content:
|
|
||||||
content = content.replace(old_health, new_health)
|
|
||||||
print('Replaced health counters')
|
|
||||||
else:
|
|
||||||
print('Could not find old_health')
|
|
||||||
|
|
||||||
with open(filepath, 'w', encoding='utf-8') as f:
|
|
||||||
f.write(content)
|
|
||||||
|
|
||||||
fix_meta()
|
|
||||||
@@ -1,44 +0,0 @@
|
|||||||
import os
|
|
||||||
import re
|
|
||||||
|
|
||||||
def fix_nvim_msgid():
|
|
||||||
filepath = 'nvim-core/src/lib.rs'
|
|
||||||
with open(filepath, 'r', encoding='utf-8') as f:
|
|
||||||
content = f.read()
|
|
||||||
|
|
||||||
# Add atomic import and static var if not exists
|
|
||||||
if 'static NEXT_MSGID' not in content:
|
|
||||||
atomic_def = "use std::sync::atomic::{AtomicU64, Ordering};\nstatic NEXT_MSGID: AtomicU64 = AtomicU64::new(1);\n"
|
|
||||||
# Find NVIM_CONN
|
|
||||||
conn_idx = content.find('static NVIM_CONN')
|
|
||||||
if conn_idx != -1:
|
|
||||||
content = content[:conn_idx] + atomic_def + content[conn_idx:]
|
|
||||||
|
|
||||||
# Replace all hardcoded msgid
|
|
||||||
# e.g., RmpValue::Integer(1.into()), // msgid
|
|
||||||
# with: let msgid = NEXT_MSGID.fetch_add(1, Ordering::SeqCst); ... RmpValue::Integer(msgid.into()),
|
|
||||||
|
|
||||||
# We need to insert `let msgid = NEXT_MSGID.fetch_add(1, Ordering::SeqCst);` before `let req = RmpValue::Array(vec![`
|
|
||||||
|
|
||||||
# We will use regex to find the blocks
|
|
||||||
funcs = [
|
|
||||||
('send_nvim_command', '1'),
|
|
||||||
('get_nvim_active_buffer', '2'),
|
|
||||||
('get_nvim_cursor', '3'),
|
|
||||||
('get_nvim_visual_selection', '4'),
|
|
||||||
('set_nvim_diagnostics', '5'),
|
|
||||||
('execute_nvim_lua', '6'),
|
|
||||||
]
|
|
||||||
|
|
||||||
for func, old_id in funcs:
|
|
||||||
pattern = rf"let req = RmpValue::Array\(vec!\[\s*RmpValue::Integer\(0\.into\(\)\),\s*RmpValue::Integer\({old_id}\.into\(\)\), // msgid"
|
|
||||||
replacement = f"let msgid = NEXT_MSGID.fetch_add(1, Ordering::SeqCst);\n let req = RmpValue::Array(vec![\n RmpValue::Integer(0.into()),\n RmpValue::Integer(msgid.into()), // msgid"
|
|
||||||
|
|
||||||
content = re.sub(pattern, replacement, content)
|
|
||||||
|
|
||||||
with open(filepath, 'w', encoding='utf-8') as f:
|
|
||||||
f.write(content)
|
|
||||||
|
|
||||||
print('Fixed msgid allocations')
|
|
||||||
|
|
||||||
fix_nvim_msgid()
|
|
||||||
@@ -1,40 +0,0 @@
|
|||||||
import os
|
|
||||||
|
|
||||||
def fix_notes():
|
|
||||||
filepath = 'server/src/handlers_v2/notes.rs'
|
|
||||||
with open(filepath, 'r', encoding='utf-8') as f:
|
|
||||||
content = f.read()
|
|
||||||
|
|
||||||
old_standup = """ let tasks = state
|
|
||||||
.tasks
|
|
||||||
.read()
|
|
||||||
.into_iter()
|
|
||||||
.filter(|t| t.updated_at >= cutoff)
|
|
||||||
.collect::<Vec<_>>();
|
|
||||||
let changes = state
|
|
||||||
.ledger
|
|
||||||
.read()
|
|
||||||
.into_iter()
|
|
||||||
.filter(|c| c.timestamp >= cutoff)
|
|
||||||
.collect::<Vec<_>>();
|
|
||||||
let summaries = state
|
|
||||||
.session_summaries
|
|
||||||
.read()
|
|
||||||
.into_iter()
|
|
||||||
.filter(|s| s.namespace == req.namespace && s.timestamp >= cutoff)
|
|
||||||
.collect::<Vec<_>>();"""
|
|
||||||
|
|
||||||
new_standup = """ let tasks = state.tasks.read_with(|items| items.iter().filter(|t| t.updated_at >= cutoff).cloned().collect::<Vec<_>>());
|
|
||||||
let changes = state.ledger.read_with(|items| items.iter().filter(|c| c.timestamp >= cutoff).cloned().collect::<Vec<_>>());
|
|
||||||
let summaries = state.session_summaries.read_with(|items| items.iter().filter(|s| s.namespace == req.namespace && s.timestamp >= cutoff).cloned().collect::<Vec<_>>());"""
|
|
||||||
|
|
||||||
if old_standup in content:
|
|
||||||
content = content.replace(old_standup, new_standup)
|
|
||||||
print('Replaced standup in notes.rs')
|
|
||||||
else:
|
|
||||||
print('Could not find old_standup in notes.rs')
|
|
||||||
|
|
||||||
with open(filepath, 'w', encoding='utf-8') as f:
|
|
||||||
f.write(content)
|
|
||||||
|
|
||||||
fix_notes()
|
|
||||||
@@ -1,66 +0,0 @@
|
|||||||
import os
|
|
||||||
|
|
||||||
def fix_stub_leaks():
|
|
||||||
filepath = 'stub/src/main.rs'
|
|
||||||
with open(filepath, 'r', encoding='utf-8') as f:
|
|
||||||
content = f.read()
|
|
||||||
|
|
||||||
# 1. Fix connect_async to handle shutdown and timeout
|
|
||||||
old_connect = """ match tokio_tungstenite::connect_async(request).await {"""
|
|
||||||
new_connect = """ let connect_result = tokio::select! {
|
|
||||||
_ = shutdown_rx.recv() => {
|
|
||||||
tracing::info!("Shutdown received during connect");
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
res = tokio::time::timeout(
|
|
||||||
tokio::time::Duration::from_secs(5),
|
|
||||||
tokio_tungstenite::connect_async(request)
|
|
||||||
) => res,
|
|
||||||
};
|
|
||||||
|
|
||||||
match connect_result {
|
|
||||||
Ok(Ok((ws_stream, _))) => {"""
|
|
||||||
|
|
||||||
if old_connect in content:
|
|
||||||
content = content.replace(old_connect, new_connect)
|
|
||||||
print("Replaced connect_async")
|
|
||||||
|
|
||||||
# Fix Err block to match the new match structure
|
|
||||||
old_err = """ Err(e) => {
|
|
||||||
tracing::error!("Failed to connect via WSS: {}", e);
|
|
||||||
tokio::time::sleep(tokio::time::Duration::from_secs(1)).await;
|
|
||||||
}"""
|
|
||||||
new_err = """ Ok(Err(e)) => {
|
|
||||||
tracing::error!("Failed to connect via WSS: {}", e);
|
|
||||||
tokio::time::sleep(tokio::time::Duration::from_secs(1)).await;
|
|
||||||
}
|
|
||||||
Err(_) => {
|
|
||||||
tracing::error!("Connection attempt timed out");
|
|
||||||
tokio::time::sleep(tokio::time::Duration::from_secs(1)).await;
|
|
||||||
}"""
|
|
||||||
|
|
||||||
if old_err in content:
|
|
||||||
content = content.replace(old_err, new_err)
|
|
||||||
print("Replaced Err branch")
|
|
||||||
|
|
||||||
# 2. Fix the break in send_task that exits the stub instead of reconnecting
|
|
||||||
old_select_send = """ _ = &mut send_task => {
|
|
||||||
tracing::error!("Send task exited");
|
|
||||||
tokio::time::sleep(tokio::time::Duration::from_millis(50)).await;
|
|
||||||
recv_task.abort();
|
|
||||||
break;
|
|
||||||
}"""
|
|
||||||
new_select_send = """ _ = &mut send_task => {
|
|
||||||
tracing::error!("Send task exited");
|
|
||||||
tokio::time::sleep(tokio::time::Duration::from_millis(50)).await;
|
|
||||||
recv_task.abort();
|
|
||||||
}"""
|
|
||||||
|
|
||||||
if old_select_send in content:
|
|
||||||
content = content.replace(old_select_send, new_select_send)
|
|
||||||
print("Replaced select send_task")
|
|
||||||
|
|
||||||
with open(filepath, 'w', encoding='utf-8') as f:
|
|
||||||
f.write(content)
|
|
||||||
|
|
||||||
fix_stub_leaks()
|
|
||||||
@@ -1,197 +0,0 @@
|
|||||||
use crate::router::McpTool;
|
|
||||||
use crate::state::MemoryState;
|
|
||||||
use std::sync::Arc;
|
|
||||||
|
|
||||||
pub struct MemoryHandler {
|
|
||||||
pub state: Arc<MemoryState>,
|
|
||||||
pub tools: std::collections::HashMap<String, Box<dyn McpTool>>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl MemoryHandler {
|
|
||||||
pub fn new(state: Arc<MemoryState>) -> Self {
|
|
||||||
let mut tools: std::collections::HashMap<String, Box<dyn McpTool>> =
|
|
||||||
std::collections::HashMap::new();
|
|
||||||
|
|
||||||
macro_rules! register {
|
|
||||||
($module:ident::$handler:ident) => {
|
|
||||||
let h = crate::handlers_v2::$module::$handler;
|
|
||||||
tools.insert(h.name().to_string(), Box::new(h));
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
register!(graph::QueryGraphPathHandler);
|
|
||||||
register!(graph::CreateEntitiesHandler);
|
|
||||||
register!(graph::CreateRelationsHandler);
|
|
||||||
register!(graph::AddObservationsHandler);
|
|
||||||
register!(graph::DeleteEntitiesHandler);
|
|
||||||
register!(graph::DeleteObservationsHandler);
|
|
||||||
register!(graph::DeleteRelationsHandler);
|
|
||||||
register!(graph::ReadGraphHandler);
|
|
||||||
register!(graph::SearchNodesHandler);
|
|
||||||
register!(graph::OpenNodesHandler);
|
|
||||||
register!(graph::VisualizeGraphHandler);
|
|
||||||
register!(graph::CondenseEntityHandler);
|
|
||||||
register!(graph::MergeEntitiesHandler);
|
|
||||||
register!(graph::FindOrphansHandler);
|
|
||||||
|
|
||||||
register!(tasks::AddTaskHandler);
|
|
||||||
register!(tasks::DeleteTaskHandler);
|
|
||||||
register!(tasks::UpdateTaskStatusHandler);
|
|
||||||
register!(tasks::ListActiveTasksHandler);
|
|
||||||
register!(tasks::SetAcceptanceCriteriaHandler);
|
|
||||||
register!(tasks::VerifyAcceptanceCriteriaHandler);
|
|
||||||
register!(tasks::AddMilestoneHandler);
|
|
||||||
register!(tasks::UpdateMilestoneHandler);
|
|
||||||
register!(tasks::ListMilestonesHandler);
|
|
||||||
|
|
||||||
register!(notes::AddStickyNoteHandler);
|
|
||||||
register!(notes::ReadStickyNotesHandler);
|
|
||||||
register!(notes::DeleteStickyNoteHandler);
|
|
||||||
register!(notes::ClearStickyNotesHandler);
|
|
||||||
register!(notes::LeaveHandoffMemoHandler);
|
|
||||||
register!(notes::ReadHandoffMemosHandler);
|
|
||||||
register!(notes::ClearHandoffMemosHandler);
|
|
||||||
register!(notes::AddSessionSummaryHandler);
|
|
||||||
register!(notes::GenerateStandupReportHandler);
|
|
||||||
|
|
||||||
register!(meta::LogDecisionHandler);
|
|
||||||
register!(meta::QueryDecisionsHandler);
|
|
||||||
register!(meta::LogErrorFixHandler);
|
|
||||||
register!(meta::SearchErrorFixesHandler);
|
|
||||||
register!(meta::LogCodeChangeHandler);
|
|
||||||
register!(meta::QueryRecentChangesHandler);
|
|
||||||
register!(meta::LearnPreferenceHandler);
|
|
||||||
register!(meta::ReadPreferencesHandler);
|
|
||||||
register!(meta::LogTechDebtHandler);
|
|
||||||
register!(meta::ResolveTechDebtHandler);
|
|
||||||
register!(meta::ListTechDebtHandler);
|
|
||||||
register!(meta::OmniSearchHandler);
|
|
||||||
register!(meta::GetProjectHealthHandler);
|
|
||||||
|
|
||||||
register!(env::UpdateEnvFingerprintHandler);
|
|
||||||
register!(env::ReadEnvFingerprintHandler);
|
|
||||||
register!(env::LogEnvRequirementHandler);
|
|
||||||
register!(env::RegisterEnvironmentHandler);
|
|
||||||
register!(env::GetEnvironmentDetailsHandler);
|
|
||||||
|
|
||||||
register!(workspaces::PinFileHandler);
|
|
||||||
register!(workspaces::UnpinFileHandler);
|
|
||||||
register!(workspaces::ListPinnedFilesHandler);
|
|
||||||
register!(workspaces::StoreSnippetHandler);
|
|
||||||
register!(workspaces::SearchSnippetsHandler);
|
|
||||||
register!(workspaces::DeleteSnippetHandler);
|
|
||||||
register!(workspaces::SaveContextWorkspaceHandler);
|
|
||||||
register!(workspaces::LoadContextWorkspaceHandler);
|
|
||||||
register!(workspaces::ListContextWorkspacesHandler);
|
|
||||||
register!(workspaces::AddPrChecklistItemHandler);
|
|
||||||
register!(workspaces::GetPrChecklistHandler);
|
|
||||||
register!(workspaces::ClearPrChecklistHandler);
|
|
||||||
|
|
||||||
Self { state, tools }
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn handle_request(&self, req: serde_json::Value) -> Option<serde_json::Value> {
|
|
||||||
let id = req.get("id").cloned().unwrap_or(serde_json::Value::Null);
|
|
||||||
let id_clone = id.clone();
|
|
||||||
let method = req.get("method").and_then(|m| m.as_str()).unwrap_or("");
|
|
||||||
|
|
||||||
match method {
|
|
||||||
"server/discover" => {
|
|
||||||
let payload = serde_json::json!({
|
|
||||||
"resultType": "complete",
|
|
||||||
"ttlMs": 0,
|
|
||||||
"cacheScope": "public",
|
|
||||||
"supportedVersions": ["2026-07-28", "2025-11-25", "2025-06-18", "2025-03-26", "2024-11-05"],
|
|
||||||
"capabilities": {
|
|
||||||
"tools": serde_json::json!({})
|
|
||||||
},
|
|
||||||
"_meta": {
|
|
||||||
"io.modelcontextprotocol/serverInfo": {
|
|
||||||
"name": "gemini-mcp-memory",
|
|
||||||
"version": "3.0.0"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
Some(crate::mcp::success(id, payload))
|
|
||||||
}
|
|
||||||
"initialize" => {
|
|
||||||
let init = rmcp::model::InitializeResult::new(
|
|
||||||
rmcp::model::ServerCapabilities::builder()
|
|
||||||
.enable_tools()
|
|
||||||
.build(),
|
|
||||||
)
|
|
||||||
.with_server_info(rmcp::model::Implementation::new(
|
|
||||||
"gemini-mcp-memory",
|
|
||||||
"3.0.0",
|
|
||||||
));
|
|
||||||
Some(crate::mcp::success(
|
|
||||||
id,
|
|
||||||
serde_json::to_value(&init).unwrap_or_default(),
|
|
||||||
))
|
|
||||||
}
|
|
||||||
"notifications/initialized" => None,
|
|
||||||
"tools/list" => {
|
|
||||||
let mut tools: Vec<serde_json::Value> =
|
|
||||||
self.tools.values().map(|t| t.schema()).collect();
|
|
||||||
tools.sort_by_key(|t| {
|
|
||||||
t.get("name")
|
|
||||||
.and_then(|n| n.as_str())
|
|
||||||
.unwrap_or("")
|
|
||||||
.to_string()
|
|
||||||
});
|
|
||||||
Some(crate::mcp::success(
|
|
||||||
id,
|
|
||||||
serde_json::json!({ "tools": tools }),
|
|
||||||
))
|
|
||||||
}
|
|
||||||
"tools/call" => {
|
|
||||||
let params = req.get("params").unwrap_or(&serde_json::Value::Null);
|
|
||||||
let name = params.get("name").and_then(|n| n.as_str()).unwrap_or("");
|
|
||||||
let args = params
|
|
||||||
.get("arguments")
|
|
||||||
.cloned()
|
|
||||||
.unwrap_or(serde_json::Value::Object(Default::default()));
|
|
||||||
|
|
||||||
self.state
|
|
||||||
.broadcast_activity(&format!("Agent executed tool: {}", name));
|
|
||||||
|
|
||||||
let result: Result<String, String> = if let Some(tool) = self.tools.get(name) {
|
|
||||||
tool.execute(args, self.state.clone()).await
|
|
||||||
} else {
|
|
||||||
Err(format!("Unknown tool: {}", name))
|
|
||||||
};
|
|
||||||
|
|
||||||
match result {
|
|
||||||
Ok(text) => {
|
|
||||||
let payload = serde_json::json!({
|
|
||||||
"content": [{"type": "text", "text": text}],
|
|
||||||
"isError": false
|
|
||||||
});
|
|
||||||
Some(crate::mcp::success(id_clone, payload))
|
|
||||||
}
|
|
||||||
Err(e) => {
|
|
||||||
tracing::error!("Tool {} failed: {}", name, e);
|
|
||||||
let payload = serde_json::json!({
|
|
||||||
"content": [{"type": "text", "text": e}],
|
|
||||||
"isError": true
|
|
||||||
});
|
|
||||||
Some(crate::mcp::success(id_clone, payload))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
m if m.starts_with("notifications/") => None,
|
|
||||||
"ping" => Some(crate::mcp::success(id, serde_json::json!({}))),
|
|
||||||
_ => {
|
|
||||||
if id.is_null() {
|
|
||||||
None
|
|
||||||
} else {
|
|
||||||
Some(crate::mcp::error(
|
|
||||||
id,
|
|
||||||
-32601,
|
|
||||||
&format!("Method {} not found", method),
|
|
||||||
))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -31,7 +31,7 @@ impl McpTool for UpdateEnvFingerprintHandler {
|
|||||||
os: std::env::consts::OS.to_string(),
|
os: std::env::consts::OS.to_string(),
|
||||||
shell: std::env::var("SHELL").unwrap_or_else(|_| "unknown".to_string()),
|
shell: std::env::var("SHELL").unwrap_or_else(|_| "unknown".to_string()),
|
||||||
tool_versions: req.tool_versions,
|
tool_versions: req.tool_versions,
|
||||||
updated_at: crate::handlers_v2::utils::now_secs(),
|
updated_at: crate::handlers::utils::now_secs(),
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
@@ -125,7 +125,7 @@ impl McpTool for RegisterEnvironmentHandler {
|
|||||||
url: req.url,
|
url: req.url,
|
||||||
description: req.description,
|
description: req.description,
|
||||||
requires_vpn: req.requires_vpn,
|
requires_vpn: req.requires_vpn,
|
||||||
updated_at: crate::handlers_v2::utils::now_secs(),
|
updated_at: crate::handlers::utils::now_secs(),
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
Ok("Environment registered".to_string())
|
Ok("Environment registered".to_string())
|
||||||
@@ -576,4 +576,4 @@ impl McpTool for FindOrphansHandler {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
use crate::handlers_v2::utils::*;
|
use crate::handlers::utils::*;
|
||||||
@@ -36,7 +36,7 @@ impl McpTool for LogDecisionHandler {
|
|||||||
context: req.context,
|
context: req.context,
|
||||||
decision: req.decision,
|
decision: req.decision,
|
||||||
consequence: req.consequence,
|
consequence: req.consequence,
|
||||||
timestamp: crate::handlers_v2::utils::now_secs(),
|
timestamp: crate::handlers::utils::now_secs(),
|
||||||
};
|
};
|
||||||
|
|
||||||
drop(idx.index_adr(&a));
|
drop(idx.index_adr(&a));
|
||||||
@@ -98,7 +98,7 @@ impl McpTool for LogErrorFixHandler {
|
|||||||
fixes.push(crate::models::ErrorFix {
|
fixes.push(crate::models::ErrorFix {
|
||||||
signature: req.signature,
|
signature: req.signature,
|
||||||
solution: req.solution,
|
solution: req.solution,
|
||||||
timestamp: crate::handlers_v2::utils::now_secs(),
|
timestamp: crate::handlers::utils::now_secs(),
|
||||||
git_commit: req.git_commit,
|
git_commit: req.git_commit,
|
||||||
git_branch: req.git_branch,
|
git_branch: req.git_branch,
|
||||||
})
|
})
|
||||||
@@ -155,7 +155,7 @@ impl McpTool for LogCodeChangeHandler {
|
|||||||
let req: LogCodeChangeTool = serde_json::from_value(args).map_err(|e| e.to_string())?;
|
let req: LogCodeChangeTool = serde_json::from_value(args).map_err(|e| e.to_string())?;
|
||||||
state.ledger.modify(|ledger| {
|
state.ledger.modify(|ledger| {
|
||||||
ledger.push(CodeChange {
|
ledger.push(CodeChange {
|
||||||
timestamp: crate::handlers_v2::utils::now_secs(),
|
timestamp: crate::handlers::utils::now_secs(),
|
||||||
file_path: req.file_path,
|
file_path: req.file_path,
|
||||||
description: req.description,
|
description: req.description,
|
||||||
git_commit: req.git_commit,
|
git_commit: req.git_commit,
|
||||||
@@ -209,7 +209,7 @@ impl McpTool for LearnPreferenceHandler {
|
|||||||
crate::models::Preference {
|
crate::models::Preference {
|
||||||
key: req.key.clone(),
|
key: req.key.clone(),
|
||||||
value: req.value,
|
value: req.value,
|
||||||
updated_at: crate::handlers_v2::utils::now_secs(),
|
updated_at: crate::handlers::utils::now_secs(),
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
@@ -258,7 +258,7 @@ impl McpTool for LogTechDebtHandler {
|
|||||||
description: req.description,
|
description: req.description,
|
||||||
ideal_solution: req.ideal_solution,
|
ideal_solution: req.ideal_solution,
|
||||||
is_resolved: false,
|
is_resolved: false,
|
||||||
created_at: crate::handlers_v2::utils::now_secs(),
|
created_at: crate::handlers::utils::now_secs(),
|
||||||
git_commit: req.git_commit,
|
git_commit: req.git_commit,
|
||||||
git_branch: req.git_branch,
|
git_branch: req.git_branch,
|
||||||
})
|
})
|
||||||
@@ -501,4 +501,4 @@ impl McpTool for GetProjectHealthHandler {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
use crate::handlers_v2::utils::*;
|
use crate::handlers::utils::*;
|
||||||
File renamed without changes.
@@ -23,7 +23,7 @@ impl McpTool for AddStickyNoteHandler {
|
|||||||
let req: AddStickyNoteTool = serde_json::from_value(args).map_err(|e| e.to_string())?;
|
let req: AddStickyNoteTool = serde_json::from_value(args).map_err(|e| e.to_string())?;
|
||||||
state.sticky.modify(|notes| {
|
state.sticky.modify(|notes| {
|
||||||
notes.push(StickyNote {
|
notes.push(StickyNote {
|
||||||
timestamp: crate::handlers_v2::utils::now_secs(),
|
timestamp: crate::handlers::utils::now_secs(),
|
||||||
content: req.content,
|
content: req.content,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -132,7 +132,7 @@ impl McpTool for LeaveHandoffMemoHandler {
|
|||||||
author: "agy".to_string(),
|
author: "agy".to_string(),
|
||||||
content: req.content,
|
content: req.content,
|
||||||
namespace: req.namespace,
|
namespace: req.namespace,
|
||||||
timestamp: crate::handlers_v2::utils::now_secs(),
|
timestamp: crate::handlers::utils::now_secs(),
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
Ok("Handoff memo left".to_string())
|
Ok("Handoff memo left".to_string())
|
||||||
@@ -219,7 +219,7 @@ impl McpTool for AddSessionSummaryHandler {
|
|||||||
summaries.push(crate::models::SessionSummary {
|
summaries.push(crate::models::SessionSummary {
|
||||||
summary: req.summary,
|
summary: req.summary,
|
||||||
namespace: req.namespace,
|
namespace: req.namespace,
|
||||||
timestamp: crate::handlers_v2::utils::now_secs(),
|
timestamp: crate::handlers::utils::now_secs(),
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
Ok("Session summary added".to_string())
|
Ok("Session summary added".to_string())
|
||||||
@@ -245,7 +245,7 @@ impl McpTool for GenerateStandupReportHandler {
|
|||||||
let req: GenerateStandupReportTool =
|
let req: GenerateStandupReportTool =
|
||||||
serde_json::from_value(args).map_err(|e| e.to_string())?;
|
serde_json::from_value(args).map_err(|e| e.to_string())?;
|
||||||
let cutoff =
|
let cutoff =
|
||||||
crate::handlers_v2::utils::now_secs().saturating_sub(req.hours_lookback * 3600);
|
crate::handlers::utils::now_secs().saturating_sub(req.hours_lookback * 3600);
|
||||||
|
|
||||||
let report_str = state.tasks.read_with(|items| {
|
let report_str = state.tasks.read_with(|items| {
|
||||||
state.ledger.read_with(|changes| {
|
state.ledger.read_with(|changes| {
|
||||||
@@ -20,7 +20,7 @@ impl McpTool for AddTaskHandler {
|
|||||||
|
|
||||||
async fn execute(&self, args: Value, state: Arc<MemoryState>) -> Result<String, String> {
|
async fn execute(&self, args: Value, state: Arc<MemoryState>) -> Result<String, String> {
|
||||||
let req: AddTaskTool = serde_json::from_value(args).map_err(|e| e.to_string())?;
|
let req: AddTaskTool = serde_json::from_value(args).map_err(|e| e.to_string())?;
|
||||||
let now = crate::handlers_v2::utils::now_secs();
|
let now = crate::handlers::utils::now_secs();
|
||||||
let task_id = uuid::Uuid::new_v4().to_string();
|
let task_id = uuid::Uuid::new_v4().to_string();
|
||||||
|
|
||||||
let deps = req.dependencies.unwrap_or_default();
|
let deps = req.dependencies.unwrap_or_default();
|
||||||
@@ -214,7 +214,7 @@ impl McpTool for UpdateTaskStatusHandler {
|
|||||||
if !blocked {
|
if !blocked {
|
||||||
// Apply update
|
// Apply update
|
||||||
tasks[target_idx].status = target_status.clone();
|
tasks[target_idx].status = target_status.clone();
|
||||||
tasks[target_idx].updated_at = crate::handlers_v2::utils::now_secs();
|
tasks[target_idx].updated_at = crate::handlers::utils::now_secs();
|
||||||
|
|
||||||
// Cascade cancellation to children
|
// Cascade cancellation to children
|
||||||
if target_status == "cancelled" || target_status == "abandoned" {
|
if target_status == "cancelled" || target_status == "abandoned" {
|
||||||
@@ -337,7 +337,7 @@ impl McpTool for SetAcceptanceCriteriaHandler {
|
|||||||
is_met: false,
|
is_met: false,
|
||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
task.updated_at = crate::handlers_v2::utils::now_secs();
|
task.updated_at = crate::handlers::utils::now_secs();
|
||||||
success = true;
|
success = true;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -381,7 +381,7 @@ impl McpTool for VerifyAcceptanceCriteriaHandler {
|
|||||||
} else {
|
} else {
|
||||||
ac.is_met = true;
|
ac.is_met = true;
|
||||||
success = true;
|
success = true;
|
||||||
task.updated_at = crate::handlers_v2::utils::now_secs();
|
task.updated_at = crate::handlers::utils::now_secs();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
File renamed without changes.
@@ -25,7 +25,7 @@ impl McpTool for PinFileHandler {
|
|||||||
pinned.push(crate::models::PinnedFile {
|
pinned.push(crate::models::PinnedFile {
|
||||||
namespace: req.namespace,
|
namespace: req.namespace,
|
||||||
file_path: req.file_path,
|
file_path: req.file_path,
|
||||||
timestamp: crate::handlers_v2::utils::now_secs(),
|
timestamp: crate::handlers::utils::now_secs(),
|
||||||
git_branch: req.git_branch,
|
git_branch: req.git_branch,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -115,7 +115,7 @@ impl McpTool for StoreSnippetHandler {
|
|||||||
language: req.language,
|
language: req.language,
|
||||||
code: req.code,
|
code: req.code,
|
||||||
description: req.description,
|
description: req.description,
|
||||||
updated_at: crate::handlers_v2::utils::now_secs(),
|
updated_at: crate::handlers::utils::now_secs(),
|
||||||
};
|
};
|
||||||
|
|
||||||
let idx = state
|
let idx = state
|
||||||
@@ -223,7 +223,7 @@ impl McpTool for SaveContextWorkspaceHandler {
|
|||||||
name: req.name,
|
name: req.name,
|
||||||
pinned_files: req.pinned_files,
|
pinned_files: req.pinned_files,
|
||||||
active_task_ids: req.active_task_ids,
|
active_task_ids: req.active_task_ids,
|
||||||
saved_at: crate::handlers_v2::utils::now_secs(),
|
saved_at: crate::handlers::utils::now_secs(),
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
Ok("Context workspace saved".to_string())
|
Ok("Context workspace saved".to_string())
|
||||||
@@ -363,4 +363,4 @@ impl McpTool for ClearPrChecklistHandler {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
use crate::handlers_v2::utils::*;
|
use crate::handlers::utils::*;
|
||||||
+129
-635
@@ -3,29 +3,28 @@
|
|||||||
allow(dead_code, unused_imports, unreachable_code)
|
allow(dead_code, unused_imports, unreachable_code)
|
||||||
)]
|
)]
|
||||||
|
|
||||||
|
mod api;
|
||||||
mod handlers;
|
mod handlers;
|
||||||
mod handlers_v2;
|
|
||||||
mod mcp;
|
mod mcp;
|
||||||
mod models;
|
mod models;
|
||||||
mod router;
|
mod router;
|
||||||
mod search;
|
mod search;
|
||||||
|
pub mod db;
|
||||||
|
pub mod error;
|
||||||
mod state;
|
mod state;
|
||||||
mod store;
|
mod store;
|
||||||
mod tools;
|
mod tools;
|
||||||
|
|
||||||
use crate::handlers::MemoryHandler;
|
use crate::api::rest::GateSetReq;
|
||||||
use crate::models::*;
|
use crate::router::MemoryHandler;
|
||||||
use crate::state::MemoryState;
|
use crate::state::MemoryState;
|
||||||
use crate::store::Store;
|
|
||||||
|
|
||||||
use redb::ReadableTable;
|
|
||||||
use std::fs;
|
|
||||||
use std::path::PathBuf;
|
|
||||||
use std::sync::{Arc, RwLock};
|
|
||||||
use std::time::Duration;
|
|
||||||
|
|
||||||
use clap::{Parser, Subcommand};
|
use clap::{Parser, Subcommand};
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
|
use std::path::PathBuf;
|
||||||
|
use std::sync::atomic::AtomicUsize;
|
||||||
|
use std::sync::{Arc, RwLock};
|
||||||
|
use std::time::Duration;
|
||||||
|
use tokio::sync::mpsc;
|
||||||
|
|
||||||
#[derive(Parser)]
|
#[derive(Parser)]
|
||||||
#[command(author, version = env!("APP_VERSION"), about = "Antigravity MCP Memory Server", long_about = None)]
|
#[command(author, version = env!("APP_VERSION"), about = "Antigravity MCP Memory Server", long_about = None)]
|
||||||
@@ -87,6 +86,12 @@ enum GateCommands {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub struct AppState {
|
||||||
|
handler: Arc<MemoryHandler>,
|
||||||
|
clients: RwLock<HashMap<String, mpsc::Sender<String>>>,
|
||||||
|
next_id: AtomicUsize,
|
||||||
|
}
|
||||||
|
|
||||||
async fn index_committer_worker(state: Arc<MemoryState>) {
|
async fn index_committer_worker(state: Arc<MemoryState>) {
|
||||||
loop {
|
loop {
|
||||||
tokio::time::sleep(Duration::from_secs(5)).await;
|
tokio::time::sleep(Duration::from_secs(5)).await;
|
||||||
@@ -98,121 +103,6 @@ async fn index_committer_worker(state: Arc<MemoryState>) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
use axum::{
|
|
||||||
Json, Router,
|
|
||||||
extract::{
|
|
||||||
Query, State,
|
|
||||||
ws::{Message, WebSocket},
|
|
||||||
},
|
|
||||||
response::IntoResponse,
|
|
||||||
routing::{get, post},
|
|
||||||
};
|
|
||||||
use futures_util::{SinkExt, StreamExt};
|
|
||||||
use std::sync::atomic::{AtomicUsize, Ordering};
|
|
||||||
use tokio::sync::mpsc;
|
|
||||||
|
|
||||||
struct AppState {
|
|
||||||
handler: Arc<MemoryHandler>,
|
|
||||||
clients: RwLock<HashMap<String, mpsc::Sender<String>>>,
|
|
||||||
next_id: AtomicUsize,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(serde::Deserialize)]
|
|
||||||
struct GateVerifyReq {
|
|
||||||
action: String,
|
|
||||||
target: String,
|
|
||||||
namespace: Option<String>,
|
|
||||||
#[serde(default)]
|
|
||||||
params: HashMap<String, String>,
|
|
||||||
#[serde(default)]
|
|
||||||
consume: bool,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(serde::Deserialize)]
|
|
||||||
struct GateSetReq {
|
|
||||||
action: String,
|
|
||||||
target: String,
|
|
||||||
namespace: Option<String>,
|
|
||||||
#[serde(default)]
|
|
||||||
params: HashMap<String, String>,
|
|
||||||
authorize: Option<bool>,
|
|
||||||
block: Option<bool>,
|
|
||||||
reason: Option<String>,
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn gate_verify_handler(
|
|
||||||
State(app_state): State<Arc<AppState>>,
|
|
||||||
Query(q): Query<GateVerifyReq>,
|
|
||||||
) -> axum::response::Response {
|
|
||||||
let mut found = None;
|
|
||||||
let mut to_remove = None;
|
|
||||||
app_state.handler.state.gates.modify(|gates| {
|
|
||||||
if let Some(idx) = gates.iter().position(|g| {
|
|
||||||
g.action == q.action
|
|
||||||
&& g.target == q.target
|
|
||||||
&& g.namespace == q.namespace
|
|
||||||
&& g.params == q.params
|
|
||||||
}) {
|
|
||||||
found = Some(gates[idx].clone());
|
|
||||||
if q.consume {
|
|
||||||
to_remove = Some(idx);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if let Some(idx) = to_remove {
|
|
||||||
gates.remove(idx);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
match found {
|
|
||||||
Some(record) => {
|
|
||||||
if record.status == "authorized" {
|
|
||||||
(axum::http::StatusCode::OK, "Authorized").into_response()
|
|
||||||
} else {
|
|
||||||
let msg = if let Some(r) = record.reason {
|
|
||||||
format!("Action blocked. Reason: {}", r)
|
|
||||||
} else {
|
|
||||||
"Action blocked.".to_string()
|
|
||||||
};
|
|
||||||
(axum::http::StatusCode::FORBIDDEN, msg).into_response()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
None => (
|
|
||||||
axum::http::StatusCode::NOT_FOUND,
|
|
||||||
"Action not yet authorized (no gate record found).",
|
|
||||||
)
|
|
||||||
.into_response(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn gate_set_handler(
|
|
||||||
State(app_state): State<Arc<AppState>>,
|
|
||||||
Json(body): Json<GateSetReq>,
|
|
||||||
) -> axum::response::Response {
|
|
||||||
let status = if body.block.unwrap_or(false) {
|
|
||||||
"blocked".to_string()
|
|
||||||
} else if body.authorize.unwrap_or(false) {
|
|
||||||
"authorized".to_string()
|
|
||||||
} else {
|
|
||||||
"pending".to_string()
|
|
||||||
};
|
|
||||||
|
|
||||||
let record = GateRecord {
|
|
||||||
id: uuid::Uuid::new_v4().to_string(),
|
|
||||||
action: body.action.clone(),
|
|
||||||
target: body.target.clone(),
|
|
||||||
namespace: body.namespace.clone(),
|
|
||||||
params: body.params.clone(),
|
|
||||||
status,
|
|
||||||
reason: body.reason.clone(),
|
|
||||||
timestamp: crate::handlers_v2::utils::now_secs(),
|
|
||||||
};
|
|
||||||
app_state.handler.state.gates.modify(|gates| {
|
|
||||||
gates.retain(|g| !(g.action == record.action && g.target == record.target));
|
|
||||||
gates.push(record);
|
|
||||||
});
|
|
||||||
(axum::http::StatusCode::OK, "Gate state updated.").into_response()
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn run_server(state: Arc<MemoryState>) -> Result<(), Box<dyn std::error::Error>> {
|
async fn run_server(state: Arc<MemoryState>) -> Result<(), Box<dyn std::error::Error>> {
|
||||||
state.rebuild_index().await;
|
state.rebuild_index().await;
|
||||||
tokio::spawn(index_committer_worker(Arc::clone(&state)));
|
tokio::spawn(index_committer_worker(Arc::clone(&state)));
|
||||||
@@ -239,191 +129,7 @@ async fn run_server(state: Arc<MemoryState>) -> Result<(), Box<dyn std::error::E
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
let app = Router::new()
|
let app = api::setup::create_router(app_state);
|
||||||
.route(
|
|
||||||
"/api/version",
|
|
||||||
get(|| async move {
|
|
||||||
axum::Json(serde_json::json!({
|
|
||||||
"version": env!("APP_VERSION"),
|
|
||||||
"git_hash": option_env!("GIT_HASH").unwrap_or("unknown")
|
|
||||||
}))
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
.route("/ws", get(ws_handler))
|
|
||||||
.route("/health", get(health_handler))
|
|
||||||
.route("/nvim/telemetry", post(nvim_telemetry_handler))
|
|
||||||
.route("/gate/verify", get(gate_verify_handler))
|
|
||||||
.route("/gate/set", post(gate_set_handler))
|
|
||||||
.route(
|
|
||||||
"/shutdown",
|
|
||||||
post(
|
|
||||||
|headers: axum::http::HeaderMap, State(state): State<Arc<AppState>>| async move {
|
|
||||||
let token_path = state.handler.state.base_dir.join("admin.token");
|
|
||||||
let expected_token = tokio::fs::read_to_string(&token_path)
|
|
||||||
.await
|
|
||||||
.unwrap_or_default()
|
|
||||||
.trim()
|
|
||||||
.to_string();
|
|
||||||
|
|
||||||
let auth_header = headers
|
|
||||||
.get(axum::http::header::AUTHORIZATION)
|
|
||||||
.and_then(|h| h.to_str().ok())
|
|
||||||
.unwrap_or_default();
|
|
||||||
|
|
||||||
if expected_token.is_empty() || auth_header != format!("Bearer {}", expected_token) {
|
|
||||||
return (axum::http::StatusCode::UNAUTHORIZED, "Unauthorized").into_response();
|
|
||||||
}
|
|
||||||
|
|
||||||
std::thread::spawn(|| {
|
|
||||||
tracing::info!(
|
|
||||||
"Received shutdown request via /shutdown endpoint. Exiting process cleanly."
|
|
||||||
);
|
|
||||||
std::thread::sleep(std::time::Duration::from_millis(100));
|
|
||||||
std::process::exit(0);
|
|
||||||
});
|
|
||||||
(axum::http::StatusCode::OK, "Shutting down...").into_response()
|
|
||||||
},
|
|
||||||
),
|
|
||||||
)
|
|
||||||
.route(
|
|
||||||
"/",
|
|
||||||
get(|| async move { axum::response::Html(include_str!("dashboard.html")) }),
|
|
||||||
)
|
|
||||||
.route(
|
|
||||||
"/api/graph",
|
|
||||||
get({
|
|
||||||
let state_clone = app_state.handler.state.clone();
|
|
||||||
move || async move {
|
|
||||||
let graph_json = state_clone.read_graph(|g| serde_json::to_string(g).unwrap_or_else(|_| "{}".to_string()));
|
|
||||||
([(axum::http::header::CONTENT_TYPE, "application/json")], graph_json)
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
.route(
|
|
||||||
"/api/tasks/{id}/complete",
|
|
||||||
post({
|
|
||||||
let state_clone = app_state.handler.state.clone();
|
|
||||||
move |axum::extract::Path(id): axum::extract::Path<String>| async move {
|
|
||||||
state_clone.tasks.modify(|tasks| {
|
|
||||||
for t in tasks.iter_mut() {
|
|
||||||
if t.id == id {
|
|
||||||
t.status = "completed".to_string();
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
axum::Json(serde_json::json!({"status": "success"}))
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
.route(
|
|
||||||
"/api/tasks",
|
|
||||||
get({
|
|
||||||
let state_clone = app_state.handler.state.clone();
|
|
||||||
move || async move {
|
|
||||||
let tasks_json = state_clone.tasks.read_with(|t| serde_json::to_string(t).unwrap_or_else(|_| "[]".to_string()));
|
|
||||||
([(axum::http::header::CONTENT_TYPE, "application/json")], tasks_json)
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
.route(
|
|
||||||
"/api/sticky",
|
|
||||||
get({
|
|
||||||
let state_clone = app_state.handler.state.clone();
|
|
||||||
move || async move {
|
|
||||||
let sticky_json = state_clone.sticky.read_with(|s| serde_json::to_string(s).unwrap_or_else(|_| "[]".to_string()));
|
|
||||||
([(axum::http::header::CONTENT_TYPE, "application/json")], sticky_json)
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
.route(
|
|
||||||
"/api/search",
|
|
||||||
get({
|
|
||||||
let state_clone = app_state.handler.state.clone();
|
|
||||||
move |axum::extract::Query(params): axum::extract::Query<
|
|
||||||
std::collections::HashMap<String, String>,
|
|
||||||
>| async move {
|
|
||||||
if let Some(q) = params.get("q")
|
|
||||||
&& let Ok(idx) = state_clone.search_index.read()
|
|
||||||
&& let Ok(results) = idx.search(q, None) {
|
|
||||||
let mut formatted_results = Vec::new();
|
|
||||||
for (id, doc_type, title, body, score) in results {
|
|
||||||
formatted_results.push(serde_json::json!({
|
|
||||||
"id": id,
|
|
||||||
"type_name": doc_type,
|
|
||||||
"title": title,
|
|
||||||
"content": body,
|
|
||||||
"score": score
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
return axum::Json(
|
|
||||||
serde_json::json!({ "results": formatted_results }),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
axum::Json(serde_json::json!({ "results": [] }))
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
.route(
|
|
||||||
"/api/activity",
|
|
||||||
get({
|
|
||||||
let state_clone = app_state.handler.state.clone();
|
|
||||||
move || async move {
|
|
||||||
let activities_json = state_clone.recent_activities.read_with(|a| serde_json::to_string(a).unwrap_or_else(|_| "[]".to_string()));
|
|
||||||
([(axum::http::header::CONTENT_TYPE, "application/json")], activities_json)
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
.route(
|
|
||||||
"/api/stats",
|
|
||||||
get({
|
|
||||||
let state_clone = app_state.handler.state.clone();
|
|
||||||
move || async move {
|
|
||||||
let (entities, relations) = state_clone.read_graph(|g| (g.entities.len(), g.relations.len()));
|
|
||||||
let tasks = state_clone.tasks.read_with(|items| items.len());
|
|
||||||
let snippets = state_clone.snippets.read_with(|items| items.len());
|
|
||||||
let tech_debts = state_clone.tech_debts.read_with(|items| items.len());
|
|
||||||
let adrs = state_clone.adrs.read_with(|items| items.len());
|
|
||||||
|
|
||||||
let ledger = state_clone.ledger.read_with(|items| items.len());
|
|
||||||
let sticky = state_clone.sticky.read_with(|items| items.len());
|
|
||||||
let error_fixes = state_clone.error_fixes.read_with(|items| items.len());
|
|
||||||
let pinned_files = state_clone.pinned_files.read_with(|items| items.len());
|
|
||||||
let session_summaries = state_clone.session_summaries.read_with(|items| items.len());
|
|
||||||
let handoff_memos = state_clone.handoff_memos.read_with(|items| items.len());
|
|
||||||
let env_fingerprints = state_clone.env_fingerprints.read_with(|items| items.len());
|
|
||||||
let env_requirements = state_clone.env_requirements.read_with(|items| items.len());
|
|
||||||
let milestones = state_clone.milestones.read_with(|items| items.len());
|
|
||||||
let environments = state_clone.environments.read_with(|items| items.len());
|
|
||||||
let pr_checklists = state_clone.pr_checklists.read_with(|items| items.len());
|
|
||||||
let gates = state_clone.gates.read_with(|items| items.len());
|
|
||||||
let context_workspaces = state_clone.context_workspaces.read_with(|items| items.len());
|
|
||||||
|
|
||||||
axum::Json(serde_json::json!({
|
|
||||||
"entities": entities,
|
|
||||||
"relations": relations,
|
|
||||||
"tasks": tasks,
|
|
||||||
"snippets": snippets,
|
|
||||||
"tech_debts": tech_debts,
|
|
||||||
"adrs": adrs,
|
|
||||||
"ledger": ledger,
|
|
||||||
"sticky": sticky,
|
|
||||||
"error_fixes": error_fixes,
|
|
||||||
"pinned_files": pinned_files,
|
|
||||||
"session_summaries": session_summaries,
|
|
||||||
"handoff_memos": handoff_memos,
|
|
||||||
"env_fingerprints": env_fingerprints,
|
|
||||||
"env_requirements": env_requirements,
|
|
||||||
"milestones": milestones,
|
|
||||||
"environments": environments,
|
|
||||||
"pr_checklists": pr_checklists,
|
|
||||||
"gates": gates,
|
|
||||||
"context_workspaces": context_workspaces
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
.with_state(app_state);
|
|
||||||
|
|
||||||
tracing::info!("MCP Memory Server running on http://127.0.0.1:3000/sse");
|
tracing::info!("MCP Memory Server running on http://127.0.0.1:3000/sse");
|
||||||
let addr = std::env::var("MCP_PORT").unwrap_or_else(|_| "3000".to_string());
|
let addr = std::env::var("MCP_PORT").unwrap_or_else(|_| "3000".to_string());
|
||||||
@@ -451,201 +157,6 @@ async fn run_server(state: Arc<MemoryState>) -> Result<(), Box<dyn std::error::E
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn ws_handler(
|
|
||||||
ws: axum::extract::ws::WebSocketUpgrade,
|
|
||||||
_headers: axum::http::HeaderMap,
|
|
||||||
axum::extract::State(state): axum::extract::State<Arc<AppState>>,
|
|
||||||
axum::extract::Query(query): axum::extract::Query<std::collections::HashMap<String, String>>,
|
|
||||||
) -> axum::response::Response {
|
|
||||||
let client_type = query
|
|
||||||
.get("client")
|
|
||||||
.cloned()
|
|
||||||
.unwrap_or_else(|| "unknown".to_string());
|
|
||||||
ws.on_upgrade(move |socket| handle_socket(socket, state, client_type))
|
|
||||||
.into_response()
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn handle_socket(socket: WebSocket, state: Arc<AppState>, _client_type: String) {
|
|
||||||
let session_id = format!("{}", state.next_id.fetch_add(1, Ordering::SeqCst));
|
|
||||||
let (tx, mut rx) = mpsc::channel::<String>(100);
|
|
||||||
|
|
||||||
state
|
|
||||||
.clients
|
|
||||||
.write()
|
|
||||||
.unwrap_or_else(|e| e.into_inner())
|
|
||||||
.insert(session_id.clone(), tx.clone());
|
|
||||||
|
|
||||||
let (mut sender, mut receiver) = socket.split();
|
|
||||||
|
|
||||||
let send_task = tokio::spawn(async move {
|
|
||||||
while let Some(msg) = rx.recv().await {
|
|
||||||
tracing::trace!(
|
|
||||||
"Sending message to websocket (length: {}): {}",
|
|
||||||
msg.len(),
|
|
||||||
msg
|
|
||||||
);
|
|
||||||
if sender.send(Message::Text(msg.into())).await.is_err() {
|
|
||||||
tracing::error!("Failed to send message to websocket");
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Premature list_changed notification removed for MCP protocol compliance
|
|
||||||
|
|
||||||
let handler = Arc::clone(&state.handler);
|
|
||||||
let state_clone = Arc::clone(&state);
|
|
||||||
let session_id_clone = session_id.clone();
|
|
||||||
|
|
||||||
let recv_task = tokio::spawn(async move {
|
|
||||||
while let Some(msg_result) = receiver.next().await {
|
|
||||||
match msg_result {
|
|
||||||
Ok(Message::Text(text)) => {
|
|
||||||
tracing::info!(
|
|
||||||
"Received text message from websocket (length: {})",
|
|
||||||
text.len()
|
|
||||||
);
|
|
||||||
tracing::trace!("Message content: {}", text);
|
|
||||||
if let Ok(payload) = serde_json::from_str::<serde_json::Value>(&text) {
|
|
||||||
// Process MCP request
|
|
||||||
if let Some(response) = handler.handle_request(payload).await {
|
|
||||||
let res_str = serde_json::to_string(&response).unwrap_or_default();
|
|
||||||
let tx_opt = state_clone
|
|
||||||
.clients
|
|
||||||
.read()
|
|
||||||
.unwrap_or_else(|e| e.into_inner())
|
|
||||||
.get(&session_id_clone)
|
|
||||||
.cloned();
|
|
||||||
if let Some(client_tx) = tx_opt {
|
|
||||||
if let Err(e) = client_tx.send(res_str).await {
|
|
||||||
tracing::error!(
|
|
||||||
"Failed to send response to client channel for session {}: {}",
|
|
||||||
session_id_clone,
|
|
||||||
e
|
|
||||||
);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
tracing::warn!(
|
|
||||||
"Could not find client_tx for session_id {} when trying to send response",
|
|
||||||
session_id_clone
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// End if let Ok(payload)
|
|
||||||
else {
|
|
||||||
tracing::warn!(
|
|
||||||
"Failed to parse payload as JSON from websocket message: {}",
|
|
||||||
text
|
|
||||||
);
|
|
||||||
}
|
|
||||||
} // End Ok(Message::Text(text))
|
|
||||||
Ok(other) => {
|
|
||||||
tracing::info!("Received non-text message from websocket: {:?}", other);
|
|
||||||
}
|
|
||||||
Err(e) => {
|
|
||||||
tracing::error!("Websocket receive error: {}", e);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
tracing::info!(
|
|
||||||
"Websocket receiver task ended for session {}",
|
|
||||||
session_id_clone
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
struct SessionCleanup {
|
|
||||||
session_id: String,
|
|
||||||
state: Arc<AppState>,
|
|
||||||
send_task: tokio::task::JoinHandle<()>,
|
|
||||||
recv_task: tokio::task::JoinHandle<()>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Drop for SessionCleanup {
|
|
||||||
fn drop(&mut self) {
|
|
||||||
self.state
|
|
||||||
.clients
|
|
||||||
.write()
|
|
||||||
.unwrap_or_else(|e| e.into_inner())
|
|
||||||
.remove(&self.session_id);
|
|
||||||
self.send_task.abort();
|
|
||||||
self.recv_task.abort();
|
|
||||||
tracing::info!(
|
|
||||||
"Websocket session {} closed and cleaned up",
|
|
||||||
self.session_id
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut cleanup = SessionCleanup {
|
|
||||||
session_id: session_id.clone(),
|
|
||||||
state: Arc::clone(&state),
|
|
||||||
send_task,
|
|
||||||
recv_task,
|
|
||||||
};
|
|
||||||
|
|
||||||
tokio::select! {
|
|
||||||
_ = &mut cleanup.send_task => {
|
|
||||||
tracing::info!("Websocket send task finished for session {}", session_id);
|
|
||||||
},
|
|
||||||
_ = &mut cleanup.recv_task => {
|
|
||||||
tracing::info!("Websocket recv task finished for session {}", session_id);
|
|
||||||
},
|
|
||||||
};
|
|
||||||
// Drop guard automatically handles removal and aborts the other task.
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(serde::Deserialize, serde::Serialize, Debug)]
|
|
||||||
pub struct NvimTelemetry {
|
|
||||||
pub session_id: String,
|
|
||||||
pub event: String,
|
|
||||||
pub file: Option<String>,
|
|
||||||
pub line: Option<i64>,
|
|
||||||
pub col: Option<i64>,
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn nvim_telemetry_handler(
|
|
||||||
State(state): State<Arc<AppState>>,
|
|
||||||
axum::Json(payload): axum::Json<NvimTelemetry>,
|
|
||||||
) -> impl axum::response::IntoResponse {
|
|
||||||
// 1. Update active_nvim.txt if FocusGained, BufEnter, or VimEnter
|
|
||||||
if payload.event == "FocusGained" || payload.event == "BufEnter" || payload.event == "VimEnter"
|
|
||||||
{
|
|
||||||
let profile =
|
|
||||||
std::env::var("USERPROFILE").unwrap_or_else(|_| "C:\\Users\\reazul.ashraf".into());
|
|
||||||
let win_path = format!("{}\\.gemini\\active_nvim.txt", profile);
|
|
||||||
let _ = tokio::fs::write(&win_path, &payload.session_id).await;
|
|
||||||
|
|
||||||
let wsl_path = "\\\\wsl.localhost\\Ubuntu\\home\\riz\\.gemini\\active_nvim.txt";
|
|
||||||
let _ = tokio::fs::write(wsl_path, &payload.session_id).await;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 2. Broadcast to UI WebSockets
|
|
||||||
let ws_msg = serde_json::json!({
|
|
||||||
"type": "nvim_telemetry",
|
|
||||||
"data": payload
|
|
||||||
});
|
|
||||||
|
|
||||||
let msg_str = ws_msg.to_string();
|
|
||||||
let senders: Vec<_> = state
|
|
||||||
.clients
|
|
||||||
.read()
|
|
||||||
.unwrap_or_else(|e| e.into_inner())
|
|
||||||
.values()
|
|
||||||
.cloned()
|
|
||||||
.collect();
|
|
||||||
for tx in senders {
|
|
||||||
let _ = tx.try_send(msg_str.clone());
|
|
||||||
}
|
|
||||||
|
|
||||||
axum::Json(serde_json::json!({"status": "ok"}))
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn health_handler() -> &'static str {
|
|
||||||
"OK"
|
|
||||||
}
|
|
||||||
|
|
||||||
fn init_logging(app_name: &str) -> Option<tracing_appender::non_blocking::WorkerGuard> {
|
fn init_logging(app_name: &str) -> Option<tracing_appender::non_blocking::WorkerGuard> {
|
||||||
let base_dir = std::env::var("MCP_MEMORY_STORE_DIR").unwrap_or_else(|_| {
|
let base_dir = std::env::var("MCP_MEMORY_STORE_DIR").unwrap_or_else(|_| {
|
||||||
dirs::home_dir()
|
dirs::home_dir()
|
||||||
@@ -696,148 +207,131 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||||||
.arg(format!("Authorization: Bearer {}", token.trim()));
|
.arg(format!("Authorization: Bearer {}", token.trim()));
|
||||||
}
|
}
|
||||||
let _ = cmd
|
let _ = cmd
|
||||||
.arg(format!("https://127.0.0.1:{}/shutdown", port))
|
.arg(format!("http://127.0.0.1:{}/shutdown", port))
|
||||||
.output();
|
.output();
|
||||||
println!("Sent shutdown request to server.");
|
|
||||||
return Ok(());
|
|
||||||
}
|
|
||||||
|
|
||||||
if cli.restart {
|
if cli.restart {
|
||||||
let port = std::env::var("MCP_PORT").unwrap_or_else(|_| "3000".to_string());
|
std::thread::sleep(Duration::from_secs(2));
|
||||||
let token = std::fs::read_to_string(base.join("admin.token")).unwrap_or_default();
|
} else {
|
||||||
let mut cmd = std::process::Command::new("curl");
|
|
||||||
cmd.arg("-k").arg("-X").arg("POST");
|
|
||||||
if !token.is_empty() {
|
|
||||||
cmd.arg("-H")
|
|
||||||
.arg(format!("Authorization: Bearer {}", token.trim()));
|
|
||||||
}
|
|
||||||
let _ = cmd
|
|
||||||
.arg(format!("https://127.0.0.1:{}/shutdown", port))
|
|
||||||
.output();
|
|
||||||
println!("Sent shutdown request to existing server. Waiting for it to exit...");
|
|
||||||
std::thread::sleep(std::time::Duration::from_millis(1500));
|
|
||||||
return Ok(());
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(target_os = "windows")]
|
|
||||||
{
|
|
||||||
use std::os::windows::process::CommandExt;
|
|
||||||
if !cli.daemon {
|
|
||||||
// Just spawn the daemon and exit. We no longer act as a proxy.
|
|
||||||
#[allow(clippy::zombie_processes)]
|
|
||||||
let _ = std::process::Command::new(
|
|
||||||
std::env::current_exe().expect("Failed to get current executable path"),
|
|
||||||
)
|
|
||||||
.arg("--daemon")
|
|
||||||
.stdin(std::process::Stdio::null())
|
|
||||||
.stdout(std::process::Stdio::null())
|
|
||||||
.stderr(std::process::Stdio::null())
|
|
||||||
.creation_flags(0x08000000) // CREATE_NO_WINDOW
|
|
||||||
.spawn()
|
|
||||||
.expect("Failed to spawn daemon");
|
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fs::create_dir_all(&base).expect("Failed to create store dir");
|
if let Some(Commands::Gate { subcmd }) = cli.command {
|
||||||
|
let port = std::env::var("MCP_PORT").unwrap_or_else(|_| "3000".to_string());
|
||||||
// Generate token
|
let rt = tokio::runtime::Runtime::new()?;
|
||||||
let admin_token = uuid::Uuid::new_v4().to_string();
|
match subcmd {
|
||||||
std::fs::write(base.join("admin.token"), &admin_token).expect("Failed to write admin token");
|
GateCommands::Set {
|
||||||
|
action,
|
||||||
let redb_path = base.join("mcp_store.redb");
|
target,
|
||||||
|
namespace,
|
||||||
let db = Arc::new(redb::Database::create(&redb_path).expect("Failed to create redb database"));
|
params,
|
||||||
|
authorize,
|
||||||
// Ensure table exists and migrate old JSON files
|
block,
|
||||||
{
|
reason,
|
||||||
let write_txn = db.begin_write().expect("Failed to begin write txn on redb");
|
} => {
|
||||||
{
|
let mut pmap = HashMap::new();
|
||||||
let mut table = write_txn
|
for p in params {
|
||||||
.open_table(crate::store::STORE_TABLE)
|
if let Some((k, v)) = p.split_once('=') {
|
||||||
.expect("Failed to open STORE_TABLE");
|
pmap.insert(k.to_string(), v.to_string());
|
||||||
|
|
||||||
let stores = vec![
|
|
||||||
("knowledge_graph_master", "knowledge_graph_master.json"),
|
|
||||||
("audit_ledger", "audit_ledger.json"),
|
|
||||||
("sticky_notes", "sticky_notes.json"),
|
|
||||||
("tasks", "tasks.json"),
|
|
||||||
("snippets", "snippets.json"),
|
|
||||||
("adrs", "adrs.json"),
|
|
||||||
("preferences", "preferences.json"),
|
|
||||||
("error_fixes", "error_fixes.json"),
|
|
||||||
("pinned_files", "pinned_files.json"),
|
|
||||||
("session_summaries", "session_summaries.json"),
|
|
||||||
("handoff_memos", "handoff_memos.json"),
|
|
||||||
("env_fingerprints", "env_fingerprints.json"),
|
|
||||||
("env_requirements", "env_requirements.json"),
|
|
||||||
("milestones", "milestones.json"),
|
|
||||||
("environments", "environments.json"),
|
|
||||||
("pr_checklists", "pr_checklists.json"),
|
|
||||||
("tech_debts", "tech_debts.json"),
|
|
||||||
("gates", "gates.json"),
|
|
||||||
("context_workspaces", "context_workspaces.json"),
|
|
||||||
];
|
|
||||||
|
|
||||||
for (key, file_name) in stores.iter() {
|
|
||||||
if table
|
|
||||||
.get(*key)
|
|
||||||
.expect("Failed to read from table")
|
|
||||||
.is_none()
|
|
||||||
{
|
|
||||||
let json_path = base.join(file_name);
|
|
||||||
if json_path.exists()
|
|
||||||
&& let Ok(data) = fs::read(&json_path)
|
|
||||||
&& serde_json::from_slice::<serde_json::Value>(&data).is_ok()
|
|
||||||
{
|
|
||||||
table
|
|
||||||
.insert(*key, data.as_slice())
|
|
||||||
.expect("Failed to insert migrated data");
|
|
||||||
let _ = fs::rename(&json_path, json_path.with_extension("json.migrated"));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
let req = GateSetReq {
|
||||||
|
action,
|
||||||
|
target,
|
||||||
|
namespace,
|
||||||
|
params: pmap,
|
||||||
|
authorize: if authorize { Some(true) } else { None },
|
||||||
|
block: if block { Some(true) } else { None },
|
||||||
|
reason,
|
||||||
|
};
|
||||||
|
rt.block_on(async {
|
||||||
|
let client = reqwest::Client::new();
|
||||||
|
let res = client
|
||||||
|
.post(format!("http://127.0.0.1:{}/gate/set", port))
|
||||||
|
.json(&req)
|
||||||
|
.send()
|
||||||
|
.await;
|
||||||
|
match res {
|
||||||
|
Ok(r) if r.status().is_success() => println!("Gate updated successfully"),
|
||||||
|
Ok(r) => println!("Failed to update gate: {}", r.status()),
|
||||||
|
Err(e) => println!("Error connecting to server: {}", e),
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
GateCommands::Verify {
|
||||||
|
action,
|
||||||
|
target,
|
||||||
|
namespace,
|
||||||
|
params: _,
|
||||||
|
consume,
|
||||||
|
} => {
|
||||||
|
let mut url = format!(
|
||||||
|
"http://127.0.0.1:{}/gate/verify?action={}&target={}&consume={}",
|
||||||
|
port, action, target, consume
|
||||||
|
);
|
||||||
|
if let Some(ns) = namespace {
|
||||||
|
url.push_str(&format!("&namespace={}", ns));
|
||||||
|
}
|
||||||
|
rt.block_on(async {
|
||||||
|
let res = reqwest::get(&url).await;
|
||||||
|
match res {
|
||||||
|
Ok(r) if r.status().is_success() => std::process::exit(0),
|
||||||
|
Ok(r) if r.status() == reqwest::StatusCode::FORBIDDEN => {
|
||||||
|
let text = r.text().await.unwrap_or_default();
|
||||||
|
eprintln!("{}", text);
|
||||||
|
std::process::exit(1);
|
||||||
|
}
|
||||||
|
Ok(r) if r.status() == reqwest::StatusCode::NOT_FOUND => {
|
||||||
|
eprintln!("Action not yet authorized.");
|
||||||
|
std::process::exit(2);
|
||||||
|
}
|
||||||
|
Ok(r) => {
|
||||||
|
eprintln!("Unexpected status: {}", r.status());
|
||||||
|
std::process::exit(3);
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
eprintln!("Error connecting to server: {}", e);
|
||||||
|
std::process::exit(4);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
write_txn.commit().expect("Failed to commit db migration");
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
|
||||||
let rt = tokio::runtime::Runtime::new().expect("Failed to create tokio runtime");
|
#[cfg(target_os = "windows")]
|
||||||
let _guard = rt.enter();
|
if cli.daemon {
|
||||||
|
let exe = std::env::current_exe()?;
|
||||||
|
std::process::Command::new("powershell")
|
||||||
|
.args([
|
||||||
|
"-WindowStyle",
|
||||||
|
"Hidden",
|
||||||
|
"-Command",
|
||||||
|
&format!(
|
||||||
|
"Start-Process -FilePath '{}' -WindowStyle Hidden",
|
||||||
|
exe.display()
|
||||||
|
),
|
||||||
|
])
|
||||||
|
.spawn()?;
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
let state = Arc::new(MemoryState {
|
let token = uuid::Uuid::new_v4().to_string();
|
||||||
graph: Store::new("knowledge_graph_master", db.clone()),
|
std::fs::write(base.join("admin.token"), &token).unwrap_or_default();
|
||||||
base_dir: base.clone(),
|
|
||||||
search_index: RwLock::new(match crate::search::MemoryIndex::new(&base) {
|
let rt = tokio::runtime::Builder::new_multi_thread()
|
||||||
Ok(idx) => idx,
|
.enable_all()
|
||||||
Err(e) => {
|
.build()
|
||||||
let log_path = dirs::home_dir()
|
.unwrap();
|
||||||
.unwrap_or_default()
|
|
||||||
.join(".gemini/mcp_memory/daemon_error.log");
|
rt.block_on(async {
|
||||||
let _ = std::fs::write(&log_path, format!("Failed to create MemoryIndex: {}\n", e));
|
let state = Arc::new(MemoryState::new(&base.to_string_lossy()));
|
||||||
std::process::exit(1);
|
if let Err(e) = run_server(state).await {
|
||||||
}
|
tracing::error!("Server error: {}", e);
|
||||||
}),
|
}
|
||||||
ledger: Store::new("audit_ledger", db.clone()),
|
|
||||||
sticky: Store::new("sticky_notes", db.clone()),
|
|
||||||
tasks: Store::new("tasks", db.clone()),
|
|
||||||
snippets: Store::new("snippets", db.clone()),
|
|
||||||
adrs: Store::new("adrs", db.clone()),
|
|
||||||
prefs: Store::new("preferences", db.clone()),
|
|
||||||
error_fixes: Store::new("error_fixes", db.clone()),
|
|
||||||
pinned_files: Store::new("pinned_files", db.clone()),
|
|
||||||
session_summaries: Store::new("session_summaries", db.clone()),
|
|
||||||
handoff_memos: Store::new("handoff_memos", db.clone()),
|
|
||||||
env_fingerprints: Store::new("env_fingerprints", db.clone()),
|
|
||||||
env_requirements: Store::new("env_requirements", db.clone()),
|
|
||||||
milestones: Store::new("milestones", db.clone()),
|
|
||||||
environments: Store::new("environments", db.clone()),
|
|
||||||
pr_checklists: Store::new("pr_checklists", db.clone()),
|
|
||||||
tech_debts: Store::new("tech_debts", db.clone()),
|
|
||||||
gates: Store::new("gates", db.clone()),
|
|
||||||
context_workspaces: Store::new("context_workspaces", db.clone()),
|
|
||||||
recent_activities: Store::new("recent_activities", db.clone()),
|
|
||||||
activity_tx: tokio::sync::broadcast::channel(100).0,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
rt.block_on(run_server(state))
|
Ok(())
|
||||||
}
|
}
|
||||||
@@ -1,173 +0,0 @@
|
|||||||
import os
|
|
||||||
import re
|
|
||||||
|
|
||||||
GROUPS = {
|
|
||||||
"graph": [
|
|
||||||
"query_graph_path", "create_entities", "create_relations", "add_observations",
|
|
||||||
"delete_entities", "delete_observations", "delete_relations", "read_graph",
|
|
||||||
"search_nodes", "open_nodes", "visualize_graph", "condense_entity",
|
|
||||||
"merge_entities", "find_orphans"
|
|
||||||
],
|
|
||||||
"tasks": [
|
|
||||||
"add_task", "delete_task", "update_task_status", "list_active_tasks",
|
|
||||||
"set_acceptance_criteria", "verify_acceptance_criteria",
|
|
||||||
"add_milestone", "update_milestone", "list_milestones"
|
|
||||||
],
|
|
||||||
"notes": [
|
|
||||||
"add_sticky_note", "read_sticky_notes", "delete_sticky_note", "clear_sticky_notes",
|
|
||||||
"leave_handoff_memo", "read_handoff_memos", "clear_handoff_memos",
|
|
||||||
"add_session_summary", "generate_standup_report"
|
|
||||||
],
|
|
||||||
"meta": [
|
|
||||||
"log_decision", "query_decisions", "log_error_fix", "search_error_fixes",
|
|
||||||
"log_code_change", "query_recent_changes", "learn_preference", "read_preferences",
|
|
||||||
"log_tech_debt", "resolve_tech_debt", "list_tech_debt", "omni_search", "get_project_health"
|
|
||||||
],
|
|
||||||
"env": [
|
|
||||||
"update_env_fingerprint", "read_env_fingerprint", "log_env_requirement",
|
|
||||||
"register_environment", "get_environment_details"
|
|
||||||
],
|
|
||||||
"workspaces": [
|
|
||||||
"pin_file", "unpin_file", "list_pinned_files", "store_snippet", "search_snippets",
|
|
||||||
"delete_snippet", "save_context_workspace", "load_context_workspace",
|
|
||||||
"list_context_workspaces", "add_pr_checklist_item", "get_pr_checklist", "clear_pr_checklist"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
def to_camel_case(snake_str):
|
|
||||||
components = snake_str.split('_')
|
|
||||||
return "".join(x.title() for x in components)
|
|
||||||
|
|
||||||
def parse_rust_match(file_path):
|
|
||||||
with open(file_path, "r", encoding="utf-8") as f:
|
|
||||||
lines = f.readlines()
|
|
||||||
|
|
||||||
start_idx = -1
|
|
||||||
for i, line in enumerate(lines):
|
|
||||||
if "let result: Result<String, String> = match name {" in line:
|
|
||||||
start_idx = i
|
|
||||||
break
|
|
||||||
|
|
||||||
if start_idx == -1:
|
|
||||||
return {}
|
|
||||||
|
|
||||||
brace_depth = 1
|
|
||||||
i = start_idx + 1
|
|
||||||
|
|
||||||
tools = {}
|
|
||||||
current_tool = None
|
|
||||||
current_body = []
|
|
||||||
|
|
||||||
while i < len(lines):
|
|
||||||
line = lines[i]
|
|
||||||
|
|
||||||
if brace_depth == 1 and "=>" in line and '"' in line:
|
|
||||||
parts = line.strip().split('"')
|
|
||||||
if len(parts) >= 3:
|
|
||||||
tool_name = parts[1]
|
|
||||||
current_tool = tool_name
|
|
||||||
current_body = []
|
|
||||||
# Don't add the "name" => { line
|
|
||||||
|
|
||||||
if current_tool is not None and not (brace_depth == 1 and "=>" in line and '"' in line):
|
|
||||||
# check if this line closes the block
|
|
||||||
next_depth = brace_depth + line.count('{') - line.count('}')
|
|
||||||
if next_depth == 1 and current_tool is not None:
|
|
||||||
# This is the closing brace
|
|
||||||
tools[current_tool] = "".join(current_body)
|
|
||||||
current_tool = None
|
|
||||||
else:
|
|
||||||
current_body.append(line)
|
|
||||||
|
|
||||||
brace_depth += line.count('{')
|
|
||||||
brace_depth -= line.count('}')
|
|
||||||
|
|
||||||
if brace_depth == 0:
|
|
||||||
break
|
|
||||||
|
|
||||||
i += 1
|
|
||||||
|
|
||||||
return tools
|
|
||||||
|
|
||||||
def transform_body(body):
|
|
||||||
# Transform parse_tool!
|
|
||||||
body = re.sub(
|
|
||||||
r'let req = parse_tool!\(args, id, ([^)]+)\);',
|
|
||||||
r'let req: \1 = serde_json::from_value(args).map_err(|e| e.to_string())?;',
|
|
||||||
body
|
|
||||||
)
|
|
||||||
# Transform handle_list_with_namespace!
|
|
||||||
def repl_handle_list(m):
|
|
||||||
store = m.group(1)
|
|
||||||
tool_type = m.group(2)
|
|
||||||
return f"""
|
|
||||||
let req: {tool_type} = serde_json::from_value(args).map_err(|e| e.to_string())?;
|
|
||||||
let mut items = state.{store}.read();
|
|
||||||
if let Some(ns) = req.namespace {{
|
|
||||||
items.retain(|i| i.namespace == ns);
|
|
||||||
}}
|
|
||||||
let data = serde_json::to_string(&items).unwrap_or_default();
|
|
||||||
return Ok(data.to_string());
|
|
||||||
"""
|
|
||||||
body = re.sub(
|
|
||||||
r'return handle_list_with_namespace!\(self, ([^,]+), ([^,]+), args, id\);',
|
|
||||||
repl_handle_list,
|
|
||||||
body
|
|
||||||
)
|
|
||||||
|
|
||||||
# Replace self.state with state
|
|
||||||
body = body.replace("self.state.", "state.")
|
|
||||||
|
|
||||||
return body
|
|
||||||
|
|
||||||
|
|
||||||
tools = parse_rust_match("server/src/handlers.rs")
|
|
||||||
|
|
||||||
for group, tool_names in GROUPS.items():
|
|
||||||
file_path = f"server/src/handlers_v2/{group}.rs"
|
|
||||||
with open(file_path, "w", encoding="utf-8") as f:
|
|
||||||
f.write("use crate::router::McpTool;\n")
|
|
||||||
f.write("use crate::state::MemoryState;\n")
|
|
||||||
f.write("use crate::tools::*;\n")
|
|
||||||
f.write("use async_trait::async_trait;\n")
|
|
||||||
f.write("use serde_json::Value;\n")
|
|
||||||
f.write("use std::sync::Arc;\n")
|
|
||||||
f.write("use std::time::{SystemTime, UNIX_EPOCH};\n\n")
|
|
||||||
|
|
||||||
for name in tool_names:
|
|
||||||
if name not in tools:
|
|
||||||
continue
|
|
||||||
|
|
||||||
body = tools[name]
|
|
||||||
# special case for query_graph_path which we already wrote properly?
|
|
||||||
# actually we will just overwrite it with the transformed body
|
|
||||||
body = transform_body(body)
|
|
||||||
|
|
||||||
struct_name = to_camel_case(name) + "Handler"
|
|
||||||
tool_type = to_camel_case(name) + "Tool"
|
|
||||||
|
|
||||||
f.write(f"pub struct {struct_name};\n\n")
|
|
||||||
f.write(f"#[async_trait]\n")
|
|
||||||
f.write(f"impl McpTool for {struct_name} {{\n")
|
|
||||||
f.write(f" fn name(&self) -> &'static str {{\n")
|
|
||||||
f.write(f' "{name}"\n')
|
|
||||||
f.write(f" }}\n\n")
|
|
||||||
f.write(f" fn schema(&self) -> Value {{\n")
|
|
||||||
# For schema description we can just put a generic one or extract it.
|
|
||||||
# I will use a generic one for now, or you can extract it from tools/list.
|
|
||||||
f.write(f' crate::mcp::tool_def::<{tool_type}>(\n')
|
|
||||||
f.write(f' "{name}",\n')
|
|
||||||
f.write(f' "Execute {name}",\n')
|
|
||||||
f.write(f' )\n')
|
|
||||||
f.write(f" }}\n\n")
|
|
||||||
f.write(f" async fn execute(&self, args: Value, state: Arc<MemoryState>) -> Result<String, String> {{\n")
|
|
||||||
f.write(body)
|
|
||||||
f.write(f" }}\n")
|
|
||||||
f.write(f"}}\n\n")
|
|
||||||
|
|
||||||
print("Generated handlers_v2 modules")
|
|
||||||
|
|
||||||
# generate mod.rs
|
|
||||||
with open("server/src/handlers_v2/mod.rs", "w", encoding="utf-8") as f:
|
|
||||||
for group in GROUPS.keys():
|
|
||||||
f.write(f"pub mod {group};\n")
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
use std::fs;
|
|
||||||
use std::io::Write;
|
|
||||||
|
|
||||||
fn main() {
|
|
||||||
let content = fs::read_to_string("server/src/handlers.rs").unwrap();
|
|
||||||
println!("Read {} bytes", content.len());
|
|
||||||
// Find the match name { block
|
|
||||||
let match_start = content.find("match name {").unwrap();
|
|
||||||
// naive extraction
|
|
||||||
println!("Found match block at {}", match_start);
|
|
||||||
}
|
|
||||||
@@ -1,82 +0,0 @@
|
|||||||
import re
|
|
||||||
import os
|
|
||||||
|
|
||||||
with open("server/src/handlers.rs", "r", encoding="utf-8") as f:
|
|
||||||
lines = f.readlines()
|
|
||||||
|
|
||||||
list_start = -1
|
|
||||||
for i, line in enumerate(lines):
|
|
||||||
if '"tools/list" => {' in line:
|
|
||||||
list_start = i
|
|
||||||
break
|
|
||||||
|
|
||||||
# Find end of tools/call
|
|
||||||
call_start = -1
|
|
||||||
for i in range(list_start, len(lines)):
|
|
||||||
if '"tools/call" => {' in line:
|
|
||||||
call_start = i
|
|
||||||
break
|
|
||||||
|
|
||||||
# Find end of tools/call
|
|
||||||
# Match brace depth from call_start
|
|
||||||
brace_depth = 1
|
|
||||||
call_end = -1
|
|
||||||
for i in range(call_start + 1, len(lines)):
|
|
||||||
brace_depth += lines[i].count('{')
|
|
||||||
brace_depth -= lines[i].count('}')
|
|
||||||
if brace_depth == 0:
|
|
||||||
call_end = i
|
|
||||||
break
|
|
||||||
|
|
||||||
# replacement block
|
|
||||||
replacement = """ "tools/list" => {
|
|
||||||
let mut tools: Vec<serde_json::Value> = self.tools.values().map(|t| t.schema()).collect();
|
|
||||||
tools.sort_by_key(|t| t.get("name").and_then(|n| n.as_str()).unwrap_or("").to_string());
|
|
||||||
Some(crate::mcp::success(
|
|
||||||
id,
|
|
||||||
serde_json::json!({ "tools": tools }),
|
|
||||||
))
|
|
||||||
}
|
|
||||||
"tools/call" => {
|
|
||||||
let params = req.get("params").unwrap_or(&serde_json::Value::Null);
|
|
||||||
let name = params.get("name").and_then(|n| n.as_str()).unwrap_or("");
|
|
||||||
let args = params
|
|
||||||
.get("arguments")
|
|
||||||
.cloned()
|
|
||||||
.unwrap_or(serde_json::Value::Object(Default::default()));
|
|
||||||
|
|
||||||
self.state
|
|
||||||
.broadcast_activity(&format!("Agent executed tool: {}", name));
|
|
||||||
|
|
||||||
let result: Result<String, String> = if let Some(tool) = self.tools.get(name) {
|
|
||||||
tool.execute(args, self.state.clone()).await
|
|
||||||
} else {
|
|
||||||
Err(format!("Unknown tool: {}", name))
|
|
||||||
};
|
|
||||||
|
|
||||||
match result {
|
|
||||||
Ok(text) => {
|
|
||||||
let payload = serde_json::json!({
|
|
||||||
"content": [{"type": "text", "text": text}],
|
|
||||||
"isError": false
|
|
||||||
});
|
|
||||||
Some(crate::mcp::success(id_clone, payload))
|
|
||||||
}
|
|
||||||
Err(e) => {
|
|
||||||
tracing::error!("Tool {} failed: {}", name, e);
|
|
||||||
let payload = serde_json::json!({
|
|
||||||
"content": [{"type": "text", "text": e}],
|
|
||||||
"isError": true
|
|
||||||
});
|
|
||||||
Some(crate::mcp::success(id_clone, payload))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
"""
|
|
||||||
|
|
||||||
new_lines = lines[:list_start] + [replacement] + lines[call_end+1:]
|
|
||||||
|
|
||||||
with open("server/src/handlers.rs", "w", encoding="utf-8") as f:
|
|
||||||
f.writelines(new_lines)
|
|
||||||
|
|
||||||
print("tools/list and tools/call replaced.")
|
|
||||||
@@ -1,106 +0,0 @@
|
|||||||
import re
|
|
||||||
import os
|
|
||||||
|
|
||||||
with open("server/src/handlers.rs", "r", encoding="utf-8") as f:
|
|
||||||
content = f.read()
|
|
||||||
|
|
||||||
# Replace MemoryHandler struct
|
|
||||||
struct_pattern = r'pub struct MemoryHandler \{\s*pub state: Arc<MemoryState>,\s*\}'
|
|
||||||
|
|
||||||
new_struct = """use crate::router::McpTool;
|
|
||||||
|
|
||||||
pub struct MemoryHandler {
|
|
||||||
pub state: Arc<MemoryState>,
|
|
||||||
pub tools: std::collections::HashMap<String, Box<dyn McpTool>>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl MemoryHandler {
|
|
||||||
pub fn new(state: Arc<MemoryState>) -> Self {
|
|
||||||
let mut tools: std::collections::HashMap<String, Box<dyn McpTool>> = std::collections::HashMap::new();
|
|
||||||
|
|
||||||
macro_rules! register {
|
|
||||||
($module:ident::$handler:ident) => {
|
|
||||||
let h = crate::handlers_v2::$module::$handler;
|
|
||||||
tools.insert(h.name().to_string(), Box::new(h));
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
register!(graph::QueryGraphPathHandler);
|
|
||||||
register!(graph::CreateEntitiesHandler);
|
|
||||||
register!(graph::CreateRelationsHandler);
|
|
||||||
register!(graph::AddObservationsHandler);
|
|
||||||
register!(graph::DeleteEntitiesHandler);
|
|
||||||
register!(graph::DeleteObservationsHandler);
|
|
||||||
register!(graph::DeleteRelationsHandler);
|
|
||||||
register!(graph::ReadGraphHandler);
|
|
||||||
register!(graph::SearchNodesHandler);
|
|
||||||
register!(graph::OpenNodesHandler);
|
|
||||||
register!(graph::VisualizeGraphHandler);
|
|
||||||
register!(graph::CondenseEntityHandler);
|
|
||||||
register!(graph::MergeEntitiesHandler);
|
|
||||||
register!(graph::FindOrphansHandler);
|
|
||||||
|
|
||||||
register!(tasks::AddTaskHandler);
|
|
||||||
register!(tasks::DeleteTaskHandler);
|
|
||||||
register!(tasks::UpdateTaskStatusHandler);
|
|
||||||
register!(tasks::ListActiveTasksHandler);
|
|
||||||
register!(tasks::SetAcceptanceCriteriaHandler);
|
|
||||||
register!(tasks::VerifyAcceptanceCriteriaHandler);
|
|
||||||
register!(tasks::AddMilestoneHandler);
|
|
||||||
register!(tasks::UpdateMilestoneHandler);
|
|
||||||
register!(tasks::ListMilestonesHandler);
|
|
||||||
|
|
||||||
register!(notes::AddStickyNoteHandler);
|
|
||||||
register!(notes::ReadStickyNotesHandler);
|
|
||||||
register!(notes::DeleteStickyNoteHandler);
|
|
||||||
register!(notes::ClearStickyNotesHandler);
|
|
||||||
register!(notes::LeaveHandoffMemoHandler);
|
|
||||||
register!(notes::ReadHandoffMemosHandler);
|
|
||||||
register!(notes::ClearHandoffMemosHandler);
|
|
||||||
register!(notes::AddSessionSummaryHandler);
|
|
||||||
register!(notes::GenerateStandupReportHandler);
|
|
||||||
|
|
||||||
register!(meta::LogDecisionHandler);
|
|
||||||
register!(meta::QueryDecisionsHandler);
|
|
||||||
register!(meta::LogErrorFixHandler);
|
|
||||||
register!(meta::SearchErrorFixesHandler);
|
|
||||||
register!(meta::LogCodeChangeHandler);
|
|
||||||
register!(meta::QueryRecentChangesHandler);
|
|
||||||
register!(meta::LearnPreferenceHandler);
|
|
||||||
register!(meta::ReadPreferencesHandler);
|
|
||||||
register!(meta::LogTechDebtHandler);
|
|
||||||
register!(meta::ResolveTechDebtHandler);
|
|
||||||
register!(meta::ListTechDebtHandler);
|
|
||||||
register!(meta::OmniSearchHandler);
|
|
||||||
register!(meta::GetProjectHealthHandler);
|
|
||||||
|
|
||||||
register!(env::UpdateEnvFingerprintHandler);
|
|
||||||
register!(env::ReadEnvFingerprintHandler);
|
|
||||||
register!(env::LogEnvRequirementHandler);
|
|
||||||
register!(env::RegisterEnvironmentHandler);
|
|
||||||
register!(env::GetEnvironmentDetailsHandler);
|
|
||||||
|
|
||||||
register!(workspaces::PinFileHandler);
|
|
||||||
register!(workspaces::UnpinFileHandler);
|
|
||||||
register!(workspaces::ListPinnedFilesHandler);
|
|
||||||
register!(workspaces::StoreSnippetHandler);
|
|
||||||
register!(workspaces::SearchSnippetsHandler);
|
|
||||||
register!(workspaces::DeleteSnippetHandler);
|
|
||||||
register!(workspaces::SaveContextWorkspaceHandler);
|
|
||||||
register!(workspaces::LoadContextWorkspaceHandler);
|
|
||||||
register!(workspaces::ListContextWorkspacesHandler);
|
|
||||||
register!(workspaces::AddPrChecklistItemHandler);
|
|
||||||
register!(workspaces::GetPrChecklistHandler);
|
|
||||||
register!(workspaces::ClearPrChecklistHandler);
|
|
||||||
|
|
||||||
Self { state, tools }
|
|
||||||
}
|
|
||||||
"""
|
|
||||||
|
|
||||||
content = re.sub(struct_pattern, new_struct, content)
|
|
||||||
content = content.replace("impl MemoryHandler {\n pub async fn handle_request", " pub async fn handle_request")
|
|
||||||
|
|
||||||
with open("server/src/handlers.rs", "w", encoding="utf-8") as f:
|
|
||||||
f.write(content)
|
|
||||||
|
|
||||||
print("MemoryHandler struct updated.")
|
|
||||||
@@ -14,3 +14,197 @@ pub trait McpTool: Send + Sync {
|
|||||||
/// Execute the tool with the given arguments
|
/// Execute the tool with the given arguments
|
||||||
async fn execute(&self, args: Value, state: Arc<MemoryState>) -> Result<String, String>;
|
async fn execute(&self, args: Value, state: Arc<MemoryState>) -> Result<String, String>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub struct MemoryHandler {
|
||||||
|
pub state: Arc<MemoryState>,
|
||||||
|
pub tools: std::collections::HashMap<String, Box<dyn McpTool>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl MemoryHandler {
|
||||||
|
pub fn new(state: Arc<MemoryState>) -> Self {
|
||||||
|
let mut tools: std::collections::HashMap<String, Box<dyn McpTool>> =
|
||||||
|
std::collections::HashMap::new();
|
||||||
|
|
||||||
|
macro_rules! register {
|
||||||
|
($module:ident::$handler:ident) => {
|
||||||
|
let h = crate::handlers::$module::$handler;
|
||||||
|
tools.insert(h.name().to_string(), Box::new(h));
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
register!(graph::QueryGraphPathHandler);
|
||||||
|
register!(graph::CreateEntitiesHandler);
|
||||||
|
register!(graph::CreateRelationsHandler);
|
||||||
|
register!(graph::AddObservationsHandler);
|
||||||
|
register!(graph::DeleteEntitiesHandler);
|
||||||
|
register!(graph::DeleteObservationsHandler);
|
||||||
|
register!(graph::DeleteRelationsHandler);
|
||||||
|
register!(graph::ReadGraphHandler);
|
||||||
|
register!(graph::SearchNodesHandler);
|
||||||
|
register!(graph::OpenNodesHandler);
|
||||||
|
register!(graph::VisualizeGraphHandler);
|
||||||
|
register!(graph::CondenseEntityHandler);
|
||||||
|
register!(graph::MergeEntitiesHandler);
|
||||||
|
register!(graph::FindOrphansHandler);
|
||||||
|
|
||||||
|
register!(tasks::AddTaskHandler);
|
||||||
|
register!(tasks::DeleteTaskHandler);
|
||||||
|
register!(tasks::UpdateTaskStatusHandler);
|
||||||
|
register!(tasks::ListActiveTasksHandler);
|
||||||
|
register!(tasks::SetAcceptanceCriteriaHandler);
|
||||||
|
register!(tasks::VerifyAcceptanceCriteriaHandler);
|
||||||
|
register!(tasks::AddMilestoneHandler);
|
||||||
|
register!(tasks::UpdateMilestoneHandler);
|
||||||
|
register!(tasks::ListMilestonesHandler);
|
||||||
|
|
||||||
|
register!(notes::AddStickyNoteHandler);
|
||||||
|
register!(notes::ReadStickyNotesHandler);
|
||||||
|
register!(notes::DeleteStickyNoteHandler);
|
||||||
|
register!(notes::ClearStickyNotesHandler);
|
||||||
|
register!(notes::LeaveHandoffMemoHandler);
|
||||||
|
register!(notes::ReadHandoffMemosHandler);
|
||||||
|
register!(notes::ClearHandoffMemosHandler);
|
||||||
|
register!(notes::AddSessionSummaryHandler);
|
||||||
|
register!(notes::GenerateStandupReportHandler);
|
||||||
|
|
||||||
|
register!(meta::LogDecisionHandler);
|
||||||
|
register!(meta::QueryDecisionsHandler);
|
||||||
|
register!(meta::LogErrorFixHandler);
|
||||||
|
register!(meta::SearchErrorFixesHandler);
|
||||||
|
register!(meta::LogCodeChangeHandler);
|
||||||
|
register!(meta::QueryRecentChangesHandler);
|
||||||
|
register!(meta::LearnPreferenceHandler);
|
||||||
|
register!(meta::ReadPreferencesHandler);
|
||||||
|
register!(meta::LogTechDebtHandler);
|
||||||
|
register!(meta::ResolveTechDebtHandler);
|
||||||
|
register!(meta::ListTechDebtHandler);
|
||||||
|
register!(meta::OmniSearchHandler);
|
||||||
|
register!(meta::GetProjectHealthHandler);
|
||||||
|
|
||||||
|
register!(env::UpdateEnvFingerprintHandler);
|
||||||
|
register!(env::ReadEnvFingerprintHandler);
|
||||||
|
register!(env::LogEnvRequirementHandler);
|
||||||
|
register!(env::RegisterEnvironmentHandler);
|
||||||
|
register!(env::GetEnvironmentDetailsHandler);
|
||||||
|
|
||||||
|
register!(workspaces::PinFileHandler);
|
||||||
|
register!(workspaces::UnpinFileHandler);
|
||||||
|
register!(workspaces::ListPinnedFilesHandler);
|
||||||
|
register!(workspaces::StoreSnippetHandler);
|
||||||
|
register!(workspaces::SearchSnippetsHandler);
|
||||||
|
register!(workspaces::DeleteSnippetHandler);
|
||||||
|
register!(workspaces::SaveContextWorkspaceHandler);
|
||||||
|
register!(workspaces::LoadContextWorkspaceHandler);
|
||||||
|
register!(workspaces::ListContextWorkspacesHandler);
|
||||||
|
register!(workspaces::AddPrChecklistItemHandler);
|
||||||
|
register!(workspaces::GetPrChecklistHandler);
|
||||||
|
register!(workspaces::ClearPrChecklistHandler);
|
||||||
|
|
||||||
|
Self { state, tools }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn handle_request(&self, req: serde_json::Value) -> Option<serde_json::Value> {
|
||||||
|
let id = req.get("id").cloned().unwrap_or(serde_json::Value::Null);
|
||||||
|
let id_clone = id.clone();
|
||||||
|
let method = req.get("method").and_then(|m| m.as_str()).unwrap_or("");
|
||||||
|
|
||||||
|
match method {
|
||||||
|
"server/discover" => {
|
||||||
|
let payload = serde_json::json!({
|
||||||
|
"resultType": "complete",
|
||||||
|
"ttlMs": 0,
|
||||||
|
"cacheScope": "public",
|
||||||
|
"supportedVersions": ["2026-07-28", "2025-11-25", "2025-06-18", "2025-03-26", "2024-11-05"],
|
||||||
|
"capabilities": {
|
||||||
|
"tools": serde_json::json!({})
|
||||||
|
},
|
||||||
|
"_meta": {
|
||||||
|
"io.modelcontextprotocol/serverInfo": {
|
||||||
|
"name": "gemini-mcp-memory",
|
||||||
|
"version": "3.0.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
Some(crate::mcp::success(id, payload))
|
||||||
|
}
|
||||||
|
"initialize" => {
|
||||||
|
let init = rmcp::model::InitializeResult::new(
|
||||||
|
rmcp::model::ServerCapabilities::builder()
|
||||||
|
.enable_tools()
|
||||||
|
.build(),
|
||||||
|
)
|
||||||
|
.with_server_info(rmcp::model::Implementation::new(
|
||||||
|
"gemini-mcp-memory",
|
||||||
|
"3.0.0",
|
||||||
|
));
|
||||||
|
Some(crate::mcp::success(
|
||||||
|
id,
|
||||||
|
serde_json::to_value(&init).unwrap_or_default(),
|
||||||
|
))
|
||||||
|
}
|
||||||
|
"notifications/initialized" => None,
|
||||||
|
"tools/list" => {
|
||||||
|
let mut tools: Vec<serde_json::Value> =
|
||||||
|
self.tools.values().map(|t| t.schema()).collect();
|
||||||
|
tools.sort_by_key(|t| {
|
||||||
|
t.get("name")
|
||||||
|
.and_then(|n| n.as_str())
|
||||||
|
.unwrap_or("")
|
||||||
|
.to_string()
|
||||||
|
});
|
||||||
|
Some(crate::mcp::success(
|
||||||
|
id,
|
||||||
|
serde_json::json!({ "tools": tools }),
|
||||||
|
))
|
||||||
|
}
|
||||||
|
"tools/call" => {
|
||||||
|
let params = req.get("params").unwrap_or(&serde_json::Value::Null);
|
||||||
|
let name = params.get("name").and_then(|n| n.as_str()).unwrap_or("");
|
||||||
|
let args = params
|
||||||
|
.get("arguments")
|
||||||
|
.cloned()
|
||||||
|
.unwrap_or(serde_json::Value::Object(Default::default()));
|
||||||
|
|
||||||
|
self.state
|
||||||
|
.broadcast_activity(&format!("Agent executed tool: {}", name));
|
||||||
|
|
||||||
|
let result: Result<String, String> = if let Some(tool) = self.tools.get(name) {
|
||||||
|
tool.execute(args, self.state.clone()).await
|
||||||
|
} else {
|
||||||
|
Err(format!("Unknown tool: {}", name))
|
||||||
|
};
|
||||||
|
|
||||||
|
match result {
|
||||||
|
Ok(text) => {
|
||||||
|
let payload = serde_json::json!({
|
||||||
|
"content": [{"type": "text", "text": text}],
|
||||||
|
"isError": false
|
||||||
|
});
|
||||||
|
Some(crate::mcp::success(id_clone, payload))
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
tracing::error!("Tool {} failed: {}", name, e);
|
||||||
|
let payload = serde_json::json!({
|
||||||
|
"content": [{"type": "text", "text": e}],
|
||||||
|
"isError": true
|
||||||
|
});
|
||||||
|
Some(crate::mcp::success(id_clone, payload))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
m if m.starts_with("notifications/") => None,
|
||||||
|
"ping" => Some(crate::mcp::success(id, serde_json::json!({}))),
|
||||||
|
_ => {
|
||||||
|
if id.is_null() {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
Some(crate::mcp::error(
|
||||||
|
id,
|
||||||
|
-32601,
|
||||||
|
&format!("Method {} not found", method),
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -32,6 +32,49 @@ pub struct MemoryState {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl MemoryState {
|
impl MemoryState {
|
||||||
|
pub fn new(base_dir_str: &str) -> Self {
|
||||||
|
let base = std::path::PathBuf::from(base_dir_str);
|
||||||
|
std::fs::create_dir_all(&base).expect("Failed to create store dir");
|
||||||
|
|
||||||
|
let db = crate::db::init_redb(&base);
|
||||||
|
|
||||||
|
Self {
|
||||||
|
graph: Store::new("knowledge_graph_master", db.clone()),
|
||||||
|
base_dir: base.clone(),
|
||||||
|
search_index: RwLock::new(match crate::search::MemoryIndex::new(&base) {
|
||||||
|
Ok(idx) => idx,
|
||||||
|
Err(e) => {
|
||||||
|
let log_path = dirs::home_dir()
|
||||||
|
.unwrap_or_default()
|
||||||
|
.join(".gemini/mcp_memory/daemon_error.log");
|
||||||
|
let _ =
|
||||||
|
std::fs::write(&log_path, format!("Failed to create MemoryIndex: {}\n", e));
|
||||||
|
std::process::exit(1);
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
ledger: Store::new("audit_ledger", db.clone()),
|
||||||
|
sticky: Store::new("sticky_notes", db.clone()),
|
||||||
|
tasks: Store::new("tasks", db.clone()),
|
||||||
|
snippets: Store::new("snippets", db.clone()),
|
||||||
|
adrs: Store::new("adrs", db.clone()),
|
||||||
|
prefs: Store::new("preferences", db.clone()),
|
||||||
|
error_fixes: Store::new("error_fixes", db.clone()),
|
||||||
|
pinned_files: Store::new("pinned_files", db.clone()),
|
||||||
|
session_summaries: Store::new("session_summaries", db.clone()),
|
||||||
|
handoff_memos: Store::new("handoff_memos", db.clone()),
|
||||||
|
env_fingerprints: Store::new("env_fingerprints", db.clone()),
|
||||||
|
env_requirements: Store::new("env_requirements", db.clone()),
|
||||||
|
milestones: Store::new("milestones", db.clone()),
|
||||||
|
environments: Store::new("environments", db.clone()),
|
||||||
|
pr_checklists: Store::new("pr_checklists", db.clone()),
|
||||||
|
tech_debts: Store::new("tech_debts", db.clone()),
|
||||||
|
gates: Store::new("gates", db.clone()),
|
||||||
|
context_workspaces: Store::new("context_workspaces", db.clone()),
|
||||||
|
recent_activities: Store::new("recent_activities", db.clone()),
|
||||||
|
activity_tx: tokio::sync::broadcast::channel(100).0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn deduplicate<T: Eq + std::hash::Hash + Clone>(input: &mut Vec<T>) {
|
pub fn deduplicate<T: Eq + std::hash::Hash + Clone>(input: &mut Vec<T>) {
|
||||||
let mut keys = std::collections::HashSet::new();
|
let mut keys = std::collections::HashSet::new();
|
||||||
input.retain(|entry| keys.insert(entry.clone()));
|
input.retain(|entry| keys.insert(entry.clone()));
|
||||||
|
|||||||
@@ -1,45 +0,0 @@
|
|||||||
use schemars::JsonSchema;
|
|
||||||
use serde::{Deserialize, Serialize};
|
|
||||||
|
|
||||||
/// Create new entities in the knowledge graph.
|
|
||||||
#[derive(Debug, Deserialize, Serialize, JsonSchema)]
|
|
||||||
pub struct CreateEntitiesTool {
|
|
||||||
pub entities: Vec<crate::models::Entity>,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Create new relations between entities in the knowledge graph.
|
|
||||||
#[derive(Debug, Deserialize, Serialize, JsonSchema)]
|
|
||||||
pub struct CreateRelationsTool {
|
|
||||||
pub relations: Vec<crate::models::Relation>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Deserialize, Serialize, JsonSchema)]
|
|
||||||
pub struct ObservationInput {
|
|
||||||
#[serde(rename = "entityName")]
|
|
||||||
pub entity_name: String,
|
|
||||||
pub contents: Vec<String>,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Add new observations to existing entities in the knowledge graph.
|
|
||||||
#[derive(Debug, Deserialize, Serialize, JsonSchema)]
|
|
||||||
pub struct AddObservationsTool {
|
|
||||||
pub observations: Vec<ObservationInput>,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Define a strict checklist of acceptance criteria for a given task or feature before starting work.
|
|
||||||
#[derive(Debug, Deserialize, Serialize, JsonSchema)]
|
|
||||||
pub struct SetAcceptanceCriteriaTool {
|
|
||||||
/// The name or title of the task/feature being worked on.
|
|
||||||
pub task_title: String,
|
|
||||||
/// An array of specific, undeniable conditions that must be proven before claiming success.
|
|
||||||
pub criteria: Vec<String>,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Mark a previously defined acceptance criteria as met by providing cryptographic-like proof (logs, output, diffs).
|
|
||||||
#[derive(Debug, Deserialize, Serialize, JsonSchema)]
|
|
||||||
pub struct VerifyAcceptanceCriteriaTool {
|
|
||||||
/// The exact text of the criteria being met.
|
|
||||||
pub criteria: String,
|
|
||||||
/// The undeniable proof (e.g., test logs, terminal output, git diff) that proves the criteria is met.
|
|
||||||
pub proof: String,
|
|
||||||
}
|
|
||||||
@@ -1,37 +0,0 @@
|
|||||||
import json
|
|
||||||
import os
|
|
||||||
|
|
||||||
eager_tools = [
|
|
||||||
"log_code_change",
|
|
||||||
"log_error_fix",
|
|
||||||
"log_tech_debt",
|
|
||||||
"omni_search",
|
|
||||||
"create_entities",
|
|
||||||
"learn_preference",
|
|
||||||
"list_active_tasks",
|
|
||||||
"add_task",
|
|
||||||
"set_acceptance_criteria",
|
|
||||||
"verify_acceptance_criteria",
|
|
||||||
"update_task_status",
|
|
||||||
"list_pinned_files",
|
|
||||||
"pin_file",
|
|
||||||
"unpin_file",
|
|
||||||
"get_project_health",
|
|
||||||
"list_tech_debt",
|
|
||||||
"resolve_tech_debt"
|
|
||||||
]
|
|
||||||
|
|
||||||
# Update Windows config
|
|
||||||
win_config_path = r'C:\Users\reazul.ashraf\.gemini\config\mcp_config.json'
|
|
||||||
with open(win_config_path, 'r', encoding='utf-8') as f:
|
|
||||||
win_data = json.load(f)
|
|
||||||
|
|
||||||
win_data['mcpServers']['mcp-memory']['eagerTools'] = eager_tools
|
|
||||||
if 'args' in win_data['mcpServers']['mcp-memory']:
|
|
||||||
# Ensure args are empty as we removed --target earlier
|
|
||||||
win_data['mcpServers']['mcp-memory']['args'] = []
|
|
||||||
|
|
||||||
with open(win_config_path, 'w', encoding='utf-8') as f:
|
|
||||||
json.dump(win_data, f, indent=2)
|
|
||||||
|
|
||||||
print("Updated Windows config")
|
|
||||||
+6
-12
@@ -95,14 +95,11 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||||||
while let Ok(msg) = rx.recv().await {
|
while let Ok(msg) = rx.recv().await {
|
||||||
let log_prefix = logger::extract_log_prefix(&msg, false);
|
let log_prefix = logger::extract_log_prefix(&msg, false);
|
||||||
tracing::info!(
|
tracing::info!(
|
||||||
">>> [Stub] Forwarding {} to server (length: {}): {}",
|
">>> [Stub] Forwarding {} to server (length: {}): {}{}",
|
||||||
log_prefix,
|
log_prefix,
|
||||||
msg.len(),
|
msg.len(),
|
||||||
if msg.len() > 1000 {
|
&msg[..std::cmp::min(msg.len(), 1000)],
|
||||||
format!("{}...", &msg[..1000])
|
if msg.len() > 1000 { "..." } else { "" }
|
||||||
} else {
|
|
||||||
msg.clone()
|
|
||||||
}
|
|
||||||
);
|
);
|
||||||
if write
|
if write
|
||||||
.send(tokio_tungstenite::tungstenite::Message::Text(msg))
|
.send(tokio_tungstenite::tungstenite::Message::Text(msg))
|
||||||
@@ -120,14 +117,11 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||||||
if let tokio_tungstenite::tungstenite::Message::Text(text) = msg {
|
if let tokio_tungstenite::tungstenite::Message::Text(text) = msg {
|
||||||
let log_prefix = logger::extract_log_prefix(&text, true);
|
let log_prefix = logger::extract_log_prefix(&text, true);
|
||||||
tracing::info!(
|
tracing::info!(
|
||||||
"<<< [Stub] Received {} from server (length: {}): {}",
|
"<<< [Stub] Received {} from server (length: {}): {}{}",
|
||||||
log_prefix,
|
log_prefix,
|
||||||
text.len(),
|
text.len(),
|
||||||
if text.len() > 1000 {
|
&text[..std::cmp::min(text.len(), 1000)],
|
||||||
format!("{}...", &text[..1000])
|
if text.len() > 1000 { "..." } else { "" }
|
||||||
} else {
|
|
||||||
text.clone()
|
|
||||||
}
|
|
||||||
);
|
);
|
||||||
use tokio::io::AsyncWriteExt;
|
use tokio::io::AsyncWriteExt;
|
||||||
let mut stdout = tokio::io::stdout();
|
let mut stdout = tokio::io::stdout();
|
||||||
|
|||||||
@@ -1,27 +0,0 @@
|
|||||||
local buf = vim.fn.bufnr('server/src/search.rs')
|
|
||||||
if buf == -1 then
|
|
||||||
vim.cmd('e server/src/search.rs')
|
|
||||||
buf = vim.api.nvim_get_current_buf()
|
|
||||||
end
|
|
||||||
|
|
||||||
vim.api.nvim_buf_set_lines(buf, 216, 222, false, {
|
|
||||||
' &self,',
|
|
||||||
' entities: &[Entity],',
|
|
||||||
' tasks: &[Task],',
|
|
||||||
' snippets: &[Snippet],',
|
|
||||||
' adrs: &[Adr],'
|
|
||||||
})
|
|
||||||
|
|
||||||
-- We need to change the loop variables inside the task from values to clones if they are passed as slices
|
|
||||||
-- Actually we can just clone the slice data before moving it into spawn_blocking
|
|
||||||
vim.api.nvim_buf_set_lines(buf, 222, 223, false, {
|
|
||||||
' ) -> tokio::task::JoinHandle<tantivy::Result<()>> {',
|
|
||||||
' let entities = entities.to_vec();',
|
|
||||||
' let tasks = tasks.to_vec();',
|
|
||||||
' let snippets = snippets.to_vec();',
|
|
||||||
' let adrs = adrs.to_vec();'
|
|
||||||
})
|
|
||||||
|
|
||||||
vim.cmd('write')
|
|
||||||
require('notify')('Updated search index_batch signature', 'info', { title = 'Antigravity' })
|
|
||||||
return 'ok'
|
|
||||||
@@ -1,28 +0,0 @@
|
|||||||
local buf = vim.fn.bufnr('server/src/search.rs')
|
|
||||||
if buf == -1 then
|
|
||||||
vim.cmd('e server/src/search.rs')
|
|
||||||
buf = vim.api.nvim_get_current_buf()
|
|
||||||
end
|
|
||||||
|
|
||||||
vim.api.nvim_buf_set_lines(buf, 216, 222, false, {
|
|
||||||
' &self,',
|
|
||||||
' entities: &[Entity],',
|
|
||||||
' tasks: &[Task],',
|
|
||||||
' snippets: &[Snippet],',
|
|
||||||
' adrs: &[Adr],'
|
|
||||||
})
|
|
||||||
|
|
||||||
-- We need to change the loop variables inside the task from values to clones if they are passed as slices
|
|
||||||
-- Actually we can just clone the slice data before moving it into spawn_blocking
|
|
||||||
vim.api.nvim_buf_set_lines(buf, 222, 223, false, {
|
|
||||||
' ) -> tokio::task::JoinHandle<tantivy::Result<()>> {',
|
|
||||||
' let entities = entities.to_vec();',
|
|
||||||
' let tasks = tasks.to_vec();',
|
|
||||||
' let snippets = snippets.to_vec();',
|
|
||||||
' let adrs = adrs.to_vec();'
|
|
||||||
})
|
|
||||||
|
|
||||||
vim.cmd('write')
|
|
||||||
require('notify')('Updated search index_batch signature', 'info', { title = 'Antigravity' })
|
|
||||||
return 'ok'
|
|
||||||
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
local code = vim.fn.readfile('temp.lua')
|
|
||||||
return table.concat(code, '\n')
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
local buf = vim.fn.bufnr('server/src/handlers_v2/graph.rs')
|
|
||||||
if buf == -1 then
|
|
||||||
vim.cmd('e server/src/handlers_v2/graph.rs')
|
|
||||||
buf = vim.api.nvim_get_current_buf()
|
|
||||||
end
|
|
||||||
return vim.api.nvim_buf_get_name(buf)
|
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
--- server/src/main.rs
|
|
||||||
+++ server/src/main.rs
|
|
||||||
@@ -225,6 +225,20 @@
|
|
||||||
next_id: AtomicUsize::new(1),
|
|
||||||
});
|
|
||||||
|
|
||||||
+ let app_state_clone = Arc::clone(&app_state);
|
|
||||||
+ let mut rx = state.activity_tx.subscribe();
|
|
||||||
+ tokio::spawn(async move {
|
|
||||||
+ while let Ok(msg) = rx.recv().await {
|
|
||||||
+ let senders: Vec<_> = app_state_clone
|
|
||||||
+ .clients
|
|
||||||
+ .read()
|
|
||||||
+ .unwrap_or_else(|e| e.into_inner())
|
|
||||||
+ .values()
|
|
||||||
+ .cloned()
|
|
||||||
+ .collect();
|
|
||||||
+ for client_tx in senders {
|
|
||||||
+ let _ = client_tx.try_send(msg.clone());
|
|
||||||
+ }
|
|
||||||
+ }
|
|
||||||
+ });
|
|
||||||
+
|
|
||||||
let app = Router::new()
|
|
||||||
.route(
|
|
||||||
Reference in new issue
Block a user