From a2febd1b00eff03e6f27f187aaaf6b350a580faa Mon Sep 17 00:00:00 2001 From: Reazul Ashraf Date: Sun, 6 Sep 2026 22:40:17 +0100 Subject: [PATCH] feat(cli): implement deterministic gatekeeper subcommand and optimize Tokio runtime --- .gitignore | 1 + Cargo.lock | 986 +++++++++++++++++++++++++++++++++++++ Cargo.toml | 16 + instructions.md | 133 +++++ src/handlers.rs | 1231 +++++++++++++++++++++++++++++++++++++++++++++++ src/main.rs | 257 ++++++++++ src/models.rs | 175 +++++++ src/state.rs | 141 ++++++ src/store.rs | 82 ++++ src/tools.rs | 364 ++++++++++++++ 10 files changed, 3386 insertions(+) create mode 100644 .gitignore create mode 100644 Cargo.lock create mode 100644 Cargo.toml create mode 100644 instructions.md create mode 100644 src/handlers.rs create mode 100644 src/main.rs create mode 100644 src/models.rs create mode 100644 src/state.rs create mode 100644 src/store.rs create mode 100644 src/tools.rs diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ea8c4bf --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +/target diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..5641e91 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,986 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "anstream" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "824a212faf96e9acacdbd09febd34438f8f711fb84e09a8916013cd7815ca28d" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000" + +[[package]] +name = "anstyle-parse" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52ce7f38b242319f7cabaa6813055467063ecdc9d355bbb4ce0c68908cd8130e" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" +dependencies = [ + "windows-sys", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" +dependencies = [ + "anstyle", + "once_cell_polyfill", + "windows-sys", +] + +[[package]] +name = "async-trait" +version = "0.1.92" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82f6aeea286b8eb4dd3431a1be1b59d290ace00f5bfd8e2a159bc2a05e2c1667" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.5", +] + +[[package]] +name = "base64" +version = "0.23.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac07cdecf99051d9a5238b80f35af32cdeba5b336e55d957b318b50137e18da5" + +[[package]] +name = "bitflags" +version = "2.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b588b76d00fde79687d7646a9b5bdf3cc0f655e0bbd080335a95d7e96f3587da" + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "bumpalo" +version = "3.20.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" + +[[package]] +name = "bytes" +version = "1.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04" + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "clap" +version = "4.6.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "473c7e07f409a8d772161724aa8db6a765a2532a70f9667eeb7b49d3d02fbdca" +dependencies = [ + "clap_builder", + "clap_derive", +] + +[[package]] +name = "clap_builder" +version = "4.6.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b48fea5a88e9ae728a2dcbedbfc0e730f7d60da42e1cb049a83c9fb8b789889" +dependencies = [ + "anstream", + "anstyle", + "clap_lex", + "strsim", +] + +[[package]] +name = "clap_derive" +version = "4.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d012d2b9d65aca7f18f4d9878a045bc17899bba951561ba5ec3c2ba1eed9a061" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn 3.0.5", +] + +[[package]] +name = "clap_lex" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9" + +[[package]] +name = "colorchoice" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570" + +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[package]] +name = "crypto-common" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "deranged" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c" + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", + "subtle", +] + +[[package]] +name = "dirs" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3e8aa94d75141228480295a7d0e7feb620b1a5ad9f12bc40be62411e38cce4e" +dependencies = [ + "dirs-sys", +] + +[[package]] +name = "dirs-sys" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e01a3366d27ee9890022452ee61b2b63a67e6f13f58900b651ff5665f0bb1fab" +dependencies = [ + "libc", + "option-ext", + "redox_users", + "windows-sys", +] + +[[package]] +name = "errno" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +dependencies = [ + "libc", + "windows-sys", +] + +[[package]] +name = "futures" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a31d2a3fbaaeb2af2368bbdd904aa8e812d3c04a1ee10d3171f52d556e5d0a3" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-channel" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1f9e3d69d39e4862ffed03ed071a76f9a13ba1d9109d355b0f0aa6b15e393c4" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92d699e522242e69e3003b94ecc1f960f3a5e015aa7c5d7486e65ad01dd94f5e" + +[[package]] +name = "futures-executor" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "031b47cf1a3c6cc8bc2fc76cd437f521619387907d469316e7c0bc278f1f5432" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-io" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53c0fa8157de1303bfffdaa1cc2a673bfffb60102f76b0ef4441659124373fed" + +[[package]] +name = "futures-macro" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fb9654ba8355388abeb8dcb4fc62f511300867002afc858860463bdd9fe0c44" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.5", +] + +[[package]] +name = "futures-sink" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1944426bf7d03f1d14f708785e4b33efd750b36d48a157b836b3efc15ede8e1d" + +[[package]] +name = "futures-task" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd417de3d1d015fc3bfd2b1ea46dfc7bab72ef86f1cc7cc9c78e728b34a6d1fd" + +[[package]] +name = "futures-util" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d50a92467f8ba5dd6e3ee5d4bd04d73ab2e4e1c44474a0674821dfce14b79bc" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "slab", +] + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "getrandom" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + +[[package]] +name = "getrandom" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099" +dependencies = [ + "cfg-if", + "libc", + "r-efi", +] + +[[package]] +name = "glob" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4eba85ea1d0a966a983acd07deee566e67395d2d96b6fb39e62b5a833f1eb0b" + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "hmac" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" +dependencies = [ + "digest", +] + +[[package]] +name = "http" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "918d3568bebf352712bc2ef3d46a8bcf1a75b373be6539de198e9105cbbf9ce0" +dependencies = [ + "bytes", + "itoa", +] + +[[package]] +name = "is_terminal_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" + +[[package]] +name = "itoa" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + +[[package]] +name = "js-sys" +version = "0.3.105" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce57d20d1ea864ce2ac172ab472d409214f4fd359f0b2a2775abdf522e2af99e" +dependencies = [ + "cfg-if", + "futures-util", + "wasm-bindgen", +] + +[[package]] +name = "libc" +version = "0.2.189" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2" + +[[package]] +name = "libredox" +version = "0.1.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d8f1ea3f21fd3405dcaf6c9b5c1630af9afc422d9073ea39c5f6d6c772e08ed" +dependencies = [ + "libc", +] + +[[package]] +name = "lock_api" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" +dependencies = [ + "scopeguard", +] + +[[package]] +name = "mcp-memory" +version = "0.1.0" +dependencies = [ + "async-trait", + "clap", + "dirs", + "glob", + "rust-mcp-sdk", + "serde", + "serde_json", + "strsim", + "tokio", + "uuid", +] + +[[package]] +name = "memchr" +version = "2.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" + +[[package]] +name = "mio" +version = "1.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b18443e9c262bfe8fa82f51666e2642c53393f7e5c27b3e1aeab922cff5b9d8" +dependencies = [ + "libc", + "wasi", + "windows-sys", +] + +[[package]] +name = "num-conv" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "521739c6d2bac4aa25192232afe6841231376b2b26d4d9fae5ecf8ca5772e441" + +[[package]] +name = "num_threads" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c7398b9c8b70908f6371f47ed36737907c87c52af34c268fed0bf0ceb92ead9" +dependencies = [ + "libc", +] + +[[package]] +name = "once_cell" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" + +[[package]] +name = "once_cell_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" + +[[package]] +name = "option-ext" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" + +[[package]] +name = "parking_lot" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-link", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + +[[package]] +name = "proc-macro2" +version = "1.0.107" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r-efi" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" + +[[package]] +name = "redox_syscall" +version = "0.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" +dependencies = [ + "bitflags", +] + +[[package]] +name = "redox_users" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4e608c6638b9c18977b00b475ac1f28d14e84b27d8d42f70e0bf1e3dec127ac" +dependencies = [ + "getrandom 0.2.17", + "libredox", + "thiserror", +] + +[[package]] +name = "rust-mcp-macros" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "58bd70356e3da5f64bb9cae7b1acc90279378c9220c6b69731d8af945938fef8" +dependencies = [ + "proc-macro2", + "quote", + "serde", + "serde_json", + "syn 3.0.5", +] + +[[package]] +name = "rust-mcp-schema" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a66664d256795cdd61b7d4d6516d8afe041c947bedd1d10aff9e41e15771b43" +dependencies = [ + "serde", + "serde_json", +] + +[[package]] +name = "rust-mcp-sdk" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9849649137179b679d8115c79716fcf03b1bd7d3b579bb61081eed59d9bbabcb" +dependencies = [ + "async-trait", + "base64", + "bytes", + "futures", + "hmac", + "http", + "rust-mcp-macros", + "rust-mcp-schema", + "rust-mcp-transport", + "serde", + "serde_json", + "sha2", + "thiserror", + "time", + "tokio", + "tracing", + "uuid", +] + +[[package]] +name = "rust-mcp-transport" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1421cf8c7524c398b94c84af024a5af59c146bdbc1d4afed076b2eac9b0a99df" +dependencies = [ + "async-trait", + "bytes", + "futures", + "rust-mcp-schema", + "serde", + "serde_json", + "thiserror", + "tokio", + "tokio-stream", + "tracing", +] + +[[package]] +name = "rustversion" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f" + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "serde" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.5", +] + +[[package]] +name = "serde_json" +version = "1.0.151" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c841b55ecdae098c80dcae9cf767f6f8a0c2cdb3416bbef72181df4d0fe73f14" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "sha2" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "signal-hook-registry" +version = "1.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b" +dependencies = [ + "errno", + "libc", +] + +[[package]] +name = "slab" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" + +[[package]] +name = "smallvec" +version = "1.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9be42f50aa861c555654aa3a37f52f4b1074bacf4e48fe0ef7fa584e80f1f0f" + +[[package]] +name = "socket2" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3d1e2c7f27f8d4cb10542a02c49005dbd6e93095799d6f3be745fae9f8fedd4" +dependencies = [ + "libc", + "windows-sys", +] + +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + +[[package]] +name = "syn" +version = "2.0.119" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "3.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12df2e0110f65b775f769bb17ef989067a1d931b2eb822bd4346631eeada89f9" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "thiserror" +version = "2.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec86235f5fcc2a73650310756d2ac5b138a5780bbbdfae3eeccec992c435ba4f" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc04cd3e1236dd4a98afca4569f2deb3f120e5422a4023be2cb683f8486292af" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.5", +] + +[[package]] +name = "time" +version = "0.3.55" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdb87b95ec50ddfa440816d227a17b2ccbdda963a316a727fda0fc4334f7d134" +dependencies = [ + "deranged", + "libc", + "num-conv", + "num_threads", + "powerfmt", + "serde_core", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e1c906769ad99c88eaa54e728060edef082f8e358ff32030cb7c7d315e81109" + +[[package]] +name = "time-macros" +version = "0.2.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e689342a48d2ea927c87ea50cabf8594854bf940e9310208848d680d668ed85" +dependencies = [ + "num-conv", + "time-core", +] + +[[package]] +name = "tokio" +version = "1.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "202caea871b69668250d242070849eb495be178ed697a3e98aebce5bc81a0bed" +dependencies = [ + "bytes", + "libc", + "mio", + "parking_lot", + "pin-project-lite", + "signal-hook-registry", + "socket2", + "tokio-macros", + "windows-sys", +] + +[[package]] +name = "tokio-macros" +version = "2.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78773a2a397f451582ce068015985c33193cf6dea8b74d2a639fe457b2f07b0e" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.5", +] + +[[package]] +name = "tokio-stream" +version = "0.1.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3d06f0b082ba57c26b79407372e57cf2a1e28124f78e9479fe80322cf53420b" +dependencies = [ + "futures-core", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "tracing" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" +dependencies = [ + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "tracing-core" +version = "0.1.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" +dependencies = [ + "once_cell", +] + +[[package]] +name = "typenum" +version = "1.20.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20" + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + +[[package]] +name = "uuid" +version = "1.26.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5772d71c9be8a8a6ac2117d949c5b224c1b72241bb611d9a3012edcf8af7812" +dependencies = [ + "getrandom 0.4.3", + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "wasm-bindgen" +version = "0.2.128" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aecb87a33d3b0c5e3b7aa46336eaf486cffafbd281b195e4c8b80d50df2351bf" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.128" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a690d511e3c1a8b3a55e33511e3c2c00c78415cd23650f32b808627f5696b9ed" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.128" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "411e4887f0071ef2d2164a9d5fdf2d20efbef78fccd3a78b0c10a1dc5295e48a" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn 3.0.5", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.128" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81941cd78d0c92026c33e5e01312845a4cb1e9af3407f9134b100dd03144103e" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + +[[package]] +name = "zmij" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b" diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..20341eb --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,16 @@ +[package] +name = "mcp-memory" +version = "0.1.0" +edition = "2024" + +[dependencies] +async-trait = "0.1.92" +clap = { version = "4.6.6", features = ["derive"] } +dirs = "6.0.0" +glob = "0.3.4" +rust-mcp-sdk = { version = "2.0.0", default-features = false, features = ["server", "macros", "stdio"] } +serde = { version = "1.0.229", features = ["derive"] } +serde_json = "1.0.151" +strsim = "0.11.1" +tokio = { version = "1.53.1", features = ["full"] } +uuid = { version = "1.26.0", features = ["v4"] } diff --git a/instructions.md b/instructions.md new file mode 100644 index 0000000..240d8cd --- /dev/null +++ b/instructions.md @@ -0,0 +1,133 @@ +# MCP Memory Server Guidelines + +This server provides an OS-agnostic, persistent Knowledge Graph and working memory. The following guidelines dictate **when** and **how** to best utilize these tools. + +## 1. Ephemeral Working Memory (Sticky Notes) +- **Tools:** dd_sticky_note, ead_sticky_notes +- **When to use:** + - When you need to remember a specific context, broken build state, or pending task across conversation boundaries (e.g., "We are in the middle of refactoring main.rs, next step is testing"). + - When saving short-term scratchpad thoughts that don't belong in the permanent knowledge graph. +- **Behavior:** Notes have a 24-hour TTL and are pruned automatically. Always consider reading sticky notes at the start of a session if picking up an ongoing task. + +## 2. The Knowledge Graph (Entities & Relations) +- **Tools:** search_nodes, open_nodes, ead_graph +- **When to use search_nodes:** Use this for **fuzzy searching** when you aren't 100% sure of the exact entity name, or you want to find everything related to a topic (e.g., "Docker", "Jenkins"). It uses Jaro-Winkler distance and token matching. +- **When to use open_nodes:** Use this for **1-hop traversal**. If you know a specific entity name (e.g., "auth-service") and want to see how it connects to other services, open_nodes will pull the entity AND all directly connected neighbors. + +## 3. Graph Maintenance & Condensation +- **Tool:** condense_entity +- **When to use:** Proactively use this tool if you notice an entity's observations array is getting excessively long (e.g., >10 items) or repetitive. Condense the list into a shorter, more concise summary of the core facts to preserve token limits. + +## 4. The Audit Ledger (Code Changes) +- **Tools:** log_code_change, query_recent_changes +- **When to use log_code_change:** *MANDATORY.* Immediately after successfully making significant file modifications (e.g., refactoring a script, fixing a bug). Log the file path and a short description. +- **When to use query_recent_changes:** When the user asks "What did we do yesterday?", "What changed recently?", or when trying to debug a newly introduced issue. (The ledger retains 7 days of history, max 1000 items). + +## 5. Architectural Visualization +- **Tool:** isualize_graph +- **When to use:** When you need to explain complex system relationships, dependencies, or architectures to the user. This tool outputs a raw Mermaid.js string. Put the output inside a ``mermaid` markdown block in an Artifact so the user can see a visual diagram. + +## 6. Structured Task Queue (Agentic Kanban) +- **Tools:** `add_task`, `update_task_status`, `list_active_tasks` +- **When to use:** Use this for structured checklist tracking of complex, multi-day features or goals (e.g., when the user initiates a `/goal`). +- **Behavior:** Log tasks with a title and description. When resuming sessions, always run `list_active_tasks` to see what is still `pending`, `in_progress`, or `blocked`, and pick up where you left off. Use `update_task_status` to transition tasks to `done`. + +## 7. Snippet & Command Vault +- **Tools:** `store_snippet`, `search_snippets`, `delete_snippet` +- **When to use:** Use the Vault to store exactly multi-line code snippets, Nushell pipelines, or complex commands that the user relies on frequently. +- **Behavior:** The vault guarantees precise syntactic preservation of the script (unlike Graph observations). Search snippets when trying to recall an exact query or pipeline. + +## 8. Context Namespaces (Project Scopes) +- **Feature:** `namespace` optional parameter +- **When to use:** When using `read_graph`, `search_nodes`, or `visualize_graph`, you can now pass `namespace` to isolate graph queries to a specific project scope (e.g. "scascanner"). +- **Behavior:** When calling `create_entities` or `create_relations`, you can inject `namespace: "your_project"` into the entity schema to isolate it from the global scope. + +## 9. Architectural Decision Records (ADRs) +- **Tools:** log_decision, query_decisions +- **When to use:** Use this whenever you make a non-trivial architectural, environmental, or tooling decision (e.g. choosing a specific framework, a specific deployment flag, bypassing a rule with a workaround). +- **Behavior:** This permanently stores the context, decision, and consequence of *why* something is done the way it is, preventing future agents from second-guessing or reverting it. + +## 10. Graph Refactoring & Maintenance +- **Tools:** merge_entities, ind_orphans +- **When to use:** Run ind_orphans periodically or when you notice graph clutter to safely delete unused nodes. Use merge_entities when you notice duplicated semantic concepts (e.g. API_Gateway vs APIGateway). +- **Behavior:** merge_entities will safely combine their observations and automatically remap all relations pointing to or from the deleted duplicate. + +## 11. Dynamic Learned Preferences +- **Tools:** learn_preference, ead_preferences +- **When to use:** When the user corrects you on a specific local nuance (e.g. "Actually, use Python 3.10 instead of 3.12 for this repo"). +- **Behavior:** Use this Key-Value store to record dynamic behavioral preferences to ensure you adapt instantly without modifying global Markdown files. + +## Error Vault / Troubleshooting +When you spend time resolving a tricky environment issue, build error, or logic bug, immediately record the fix to save future time. +- **log_error_fix:** Provide the signature (the exact error string or stack trace snippet) and the solution. +- **search_error_fixes:** When encountering a weird bug, query this vault first before debugging from scratch. + +## Pinned Workspaces (Hot Files) +To maintain focus on the active "working set" of files in large repositories, use pins. +- **pin_file / unpin_file:** Pin the 3-5 files you are actively modifying to the current +amespace (project). +- **list_pinned_files:** When starting a new session or returning to a project, always list pinned files first to instantly regain context on what was being worked on. + +## Rolling Session Summaries (Project Timeline) +To maintain a chronological narrative of the project's evolution beyond just code diffs. +- **add_session_summary:** At the end of every major coding session, write a 2-sentence summary of what was accomplished and add it to the +amespace. +- **get_project_timeline:** When rejoining a project after a long time, read the timeline to instantly understand the recent architectural history. + +## Agent Handoffs (The Inbox) +When working in a multi-session or multi-agent environment, agents need to communicate context across time. +- **leave_handoff_memo:** Leave a quick message describing current progress, roadblocks, or the literal next step to take. +- **read_handoff_memos:** ALWAYS check for memos when waking up in a new project namespace. +- **clear_handoff_memos:** Clear the memo after you have successfully read it and absorbed its context. + +## Environment & Blueprint Tracker +Stop wasting tokens rediscovering how to run or configure the project. +- **update_env_fingerprint:** Run this when you set up a new project to snapshot the OS, shell, and key language versions (e.g. python, ustc). +- **read_env_fingerprint:** Query this to instantly know how the project is run. +- **log_env_requirement:** Log required .env variables (e.g. DATABASE_URL) without logging the secret itself. + +## Milestones (Epics) +Organize granular tasks into high-level phases. +- **add_milestone:** Group a large subset of work into a cohesive phase (e.g., "V1 MVP", "CI/CD Setup"). +- **update_milestone:** Mark a milestone as active, blocked, or completed. +- **list_milestones:** Use this to ensure task priorities align with the current active milestone. + +## Standup Reports +When asked for a progress update or standup report, do not guess or read raw git logs. +- **generate_standup_report:** Generates a structured JSON containing all asks updated, code_changes logged, and session_summaries added within the last N hours. Format this cleanly as a markdown report for the user. + +## Infrastructure & Environment Registry +Avoid asking the user for URLs or connection details repeatedly. +- **register_environment:** Save connection details for Dev, QA, Staging, or Prod environments. +- **get_environment_details:** Query this to know how to connect to databases, APIs, or VPNs. + +## Pre-Push / PR Quality Checklists +Ensure high code quality and prevent incomplete pull requests. +- **add_pr_checklist_item:** Add recurring repository chores (e.g., "Run cargo fmt", "Update CHANGELOG"). +- **get_pr_checklist:** Query and explicitly verify EVERY item on this list before triggering git push or merging PRs. +- **clear_pr_checklist:** Clear if the project lifecycle changes dramatically. + +## Tech Debt & Refactor Backlog +Keep the main task board clean by isolating "hacky" workarounds. +- **log_tech_debt:** Record why a shortcut was taken and what the ideal solution should be. +- **resolve_tech_debt:** Mark a debt as paid off once refactored. +- **list_tech_debt:** Query this before starting a refactoring session. + +## Context Workspaces +When shifting context rapidly (e.g. from building a feature to fixing a prod bug), save your state. +- **save_context_workspace:** Save your active pinned files and task IDs under a named workspace (e.g., "Feature X"). +- **load_context_workspace:** Retrieve a saved workspace to instantly restore your context when returning to that task. +- **list_context_workspaces:** List all saved workspaces in a project. + +## Omni-Search (Global Vault Search) +When you remember a vague keyword but don't know which specific vault it's stored in. +- **omni_search:** Searches across the Knowledge Graph, Tasks, Snippets, ADRs, Tech Debt, Memos, and Error Fixes simultaneously. + +## Project Health Dashboard +When starting a new session, get a numerical aggregate of the project's current state. +- **get_project_health:** Returns a quick digest of active tasks, unread memos, unresolved tech debt, and pending PR checklist items. + +## Git Context Binding (VCS Sync) +To maintain absolute traceability, we link memory items directly to the exact git commits they occurred on. +- When calling **log_code_change**, **log_error_fix**, or **log_tech_debt**, you should execute git rev-parse HEAD and git branch --show-current in the project directory first (if it's a git repo). +- Pass the resulting hash and branch name into the git_commit and git_branch arguments of those tools to permanently link the memory item to the VCS state. diff --git a/src/handlers.rs b/src/handlers.rs new file mode 100644 index 0000000..b54f393 --- /dev/null +++ b/src/handlers.rs @@ -0,0 +1,1231 @@ +use crate::models::*; +use crate::state::MemoryState; +use crate::tools::*; +use async_trait::async_trait; +use rust_mcp_sdk::{ + McpServer, RequestContext, + mcp_server::ServerHandler, + schema::{ + CallToolRequestParams, CallToolResult, ListToolsResult, ListToolsResultCacheScope, + PaginatedRequestParams, RpcError, ServerResult, schema_utils::CallToolError, + }, +}; +use serde::{Deserialize, de::DeserializeOwned}; +use std::collections::HashSet; +use std::sync::Arc; +use std::time::{SystemTime, UNIX_EPOCH}; + +fn parse_args( + args: Option>, +) -> std::result::Result { + serde_json::from_value(serde_json::Value::Object(args.unwrap_or_default())) + .map_err(|_| CallToolError::unknown_tool("Invalid args")) +} + +pub struct MemoryHandler { + pub state: Arc, +} + +#[async_trait] +impl ServerHandler for MemoryHandler { + async fn handle_list_tools_request( + &self, + _r: Option, + _c: &RequestContext, + _rt: Arc, + ) -> std::result::Result { + Ok(ListToolsResult { + tools: vec![ + CreateEntitiesTool::tool(), + CreateRelationsTool::tool(), + AddObservationsTool::tool(), + DeleteEntitiesTool::tool(), + DeleteObservationsTool::tool(), + DeleteRelationsTool::tool(), + ReadGraphTool::tool(), + SearchNodesTool::tool(), + OpenNodesTool::tool(), + LogCodeChangeTool::tool(), + QueryRecentChangesTool::tool(), + VisualizeGraphTool::tool(), + AddStickyNoteTool::tool(), + ReadStickyNotesTool::tool(), + CondenseEntityTool::tool(), + AddTaskTool::tool(), + UpdateTaskStatusTool::tool(), + ListActiveTasksTool::tool(), + StoreSnippetTool::tool(), + SearchSnippetsTool::tool(), + DeleteSnippetTool::tool(), + LogDecisionTool::tool(), + QueryDecisionsTool::tool(), + MergeEntitiesTool::tool(), + FindOrphansTool::tool(), + LearnPreferenceTool::tool(), + ReadPreferencesTool::tool(), + LogErrorFixTool::tool(), + SearchErrorFixesTool::tool(), + PinFileTool::tool(), + UnpinFileTool::tool(), + ListPinnedFilesTool::tool(), + AddSessionSummaryTool::tool(), + GetProjectTimelineTool::tool(), + LeaveHandoffMemoTool::tool(), + ReadHandoffMemosTool::tool(), + ClearHandoffMemosTool::tool(), + UpdateEnvFingerprintTool::tool(), + ReadEnvFingerprintTool::tool(), + LogEnvRequirementTool::tool(), + AddMilestoneTool::tool(), + UpdateMilestoneTool::tool(), + ListMilestonesTool::tool(), + GenerateStandupReportTool::tool(), + RegisterEnvironmentTool::tool(), + GetEnvironmentDetailsTool::tool(), + AddPrChecklistItemTool::tool(), + GetPrChecklistTool::tool(), + ClearPrChecklistTool::tool(), + LogTechDebtTool::tool(), + ResolveTechDebtTool::tool(), + ListTechDebtTool::tool(), + SaveContextWorkspaceTool::tool(), + LoadContextWorkspaceTool::tool(), + ListContextWorkspacesTool::tool(), + OmniSearchTool::tool(), + GetProjectHealthTool::tool(), + ], + cache_scope: ListToolsResultCacheScope::Private, + result_type: "complete".to_string(), + ttl_ms: 0, + meta: None, + next_cursor: None, + }) + } + async fn handle_call_tool_request( + &self, + params: CallToolRequestParams, + _c: &RequestContext, + _rt: Arc, + ) -> std::result::Result { + let args = params.arguments; + match params.name.as_str() { + "create_entities" => { + let req: CreateEntitiesTool = parse_args(args)?; + self.state.write_to_local_delta(|g| { + for e_val in req.entities { + if let Some(e) = if e_val.is_string() { + serde_json::from_str(e_val.as_str().unwrap()).ok() + } else { + serde_json::from_value(e_val).ok() + } { + let entity: Entity = e; + if !entity.name.is_empty() { + g.entities.insert(entity.name.clone(), entity); + } + } + } + }); + Ok(ServerResult::from(CallToolResult::text_content(vec![ + "Entities created".into(), + ]))) + } + "create_relations" => { + let req: CreateRelationsTool = parse_args(args)?; + self.state.write_to_local_delta(|g| { + for r_val in req.relations { + if let Some(r) = if r_val.is_string() { + serde_json::from_str(r_val.as_str().unwrap()).ok() + } else { + serde_json::from_value(r_val).ok() + } { + let relation: Relation = r; + if !relation.from.is_empty() && !relation.to.is_empty() { + g.relations.push(relation); + } + } + } + }); + Ok(ServerResult::from(CallToolResult::text_content(vec![ + "Relations created".into(), + ]))) + } + "add_observations" => { + let req: AddObservationsTool = parse_args(args)?; + #[derive(Deserialize)] + struct ObsInput { + #[serde(rename = "entityName")] + entity_name: String, + contents: Vec, + } + let full = self.state.get_full_graph(); + self.state.write_to_local_delta(|g| { + for o_val in req.observations { + if let Some(o) = if o_val.is_string() { + serde_json::from_str::(o_val.as_str().unwrap()).ok() + } else { + serde_json::from_value(o_val).ok() + } && let Some(full_e) = full.entities.get(&o.entity_name) + { + let mut e = + g.entities + .get(&o.entity_name) + .cloned() + .unwrap_or_else(|| Entity { + name: o.entity_name.clone(), + entity_type: full_e.entity_type.clone(), + observations: vec![], + namespace: full_e.namespace.clone(), + }); + e.observations.extend(o.contents); + g.entities.insert(o.entity_name, e); + } + } + }); + Ok(ServerResult::from(CallToolResult::text_content(vec![ + "Observations added".into(), + ]))) + } + "delete_entities" => { + let req: DeleteEntitiesTool = parse_args(args)?; + let to_delete: HashSet<_> = req.entity_names.into_iter().collect(); + self.state.apply_sync_write(|master| { + for name in &to_delete { + master.entities.remove(name); + } + master + .relations + .retain(|r| !to_delete.contains(&r.from) && !to_delete.contains(&r.to)); + }); + Ok(ServerResult::from(CallToolResult::text_content(vec![ + "Entities deleted".into(), + ]))) + } + "delete_observations" => { + let req: DeleteObservationsTool = parse_args(args)?; + #[derive(Deserialize)] + struct ObsDel { + #[serde(rename = "entityName")] + entity_name: String, + observations: Vec, + } + self.state.apply_sync_write(|master| { + for d_val in req.deletions { + if let Some(d) = if d_val.is_string() { + serde_json::from_str::(d_val.as_str().unwrap()).ok() + } else { + serde_json::from_value(d_val).ok() + } && let Some(e) = master.entities.get_mut(&d.entity_name) + { + let to_rem: HashSet<_> = d.observations.into_iter().collect(); + e.observations.retain(|o| !to_rem.contains(o)); + } + } + }); + Ok(ServerResult::from(CallToolResult::text_content(vec![ + "Observations deleted".into(), + ]))) + } + "delete_relations" => { + let req: DeleteRelationsTool = parse_args(args)?; + self.state.apply_sync_write(|master| { + let mut to_rem = HashSet::new(); + for r_val in req.relations { + if let Some(r) = if r_val.is_string() { + serde_json::from_str::(r_val.as_str().unwrap()).ok() + } else { + serde_json::from_value(r_val).ok() + } { + to_rem.insert(format!( + "{}|{}|{}|{}", + r.from, r.to, r.relation_type, r.namespace + )); + } + } + master.relations.retain(|r| { + !to_rem.contains(&format!( + "{}|{}|{}|{}", + r.from, r.to, r.relation_type, r.namespace + )) + }); + }); + Ok(ServerResult::from(CallToolResult::text_content(vec![ + "Relations deleted".into(), + ]))) + } + "read_graph" => { + let req: ReadGraphTool = parse_args(args)?; + let mut full = self.state.get_full_graph(); + if let Some(ns) = req.namespace { + full.entities.retain(|_, e| e.namespace == ns); + full.relations.retain(|r| r.namespace == ns); + } + let data = serde_json::to_string(&full).unwrap_or_default(); + Ok(ServerResult::from(CallToolResult::text_content(vec![ + data.into(), + ]))) + } + "search_nodes" => { + let req: SearchNodesTool = parse_args(args)?; + let query = req.query.to_lowercase(); + let tokens: Vec<&str> = query.split_whitespace().collect(); + let mut result = KnowledgeGraph::default(); + for (name, e) in self.state.get_full_graph().entities { + if let Some(ns) = &req.namespace + && e.namespace != *ns + { + continue; + } + let name_low = name.to_lowercase(); + let type_low = e.entity_type.to_lowercase(); + let full_txt = format!( + "{} {} {}", + name_low, + type_low, + e.observations.join(" ").to_lowercase() + ); + let matched = full_txt.contains(&query) + || (!tokens.is_empty() && tokens.iter().all(|&t| full_txt.contains(t))) + || strsim::jaro_winkler(&name_low, &query) > 0.85 + || strsim::jaro_winkler(&type_low, &query) > 0.85; + if matched { + result.entities.insert(name, e); + } + } + let data = serde_json::to_string(&result).unwrap_or_default(); + Ok(ServerResult::from(CallToolResult::text_content(vec![ + data.into(), + ]))) + } + "open_nodes" => { + let req: OpenNodesTool = parse_args(args)?; + let targets: HashSet<_> = req.names.into_iter().collect(); + let full = self.state.get_full_graph(); + let mut result = KnowledgeGraph::default(); + let mut connected = HashSet::new(); + for r in &full.relations { + if targets.contains(&r.from) { + connected.insert(r.to.clone()); + result.relations.push(r.clone()); + } else if targets.contains(&r.to) { + connected.insert(r.from.clone()); + result.relations.push(r.clone()); + } + } + for (name, e) in full.entities { + if targets.contains(&name) || connected.contains(&name) { + result.entities.insert(name, e); + } + } + let data = serde_json::to_string(&result).unwrap_or_default(); + Ok(ServerResult::from(CallToolResult::text_content(vec![ + data.into(), + ]))) + } + "log_code_change" => { + let req: LogCodeChangeTool = parse_args(args)?; + self.state.ledger.modify(|ledger| { + ledger.push(CodeChange { + timestamp: SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap() + .as_secs(), + file_path: req.file_path, + description: req.description, + git_commit: req.git_commit, + git_branch: req.git_branch, + }); + }); + Ok(ServerResult::from(CallToolResult::text_content(vec![ + "Code change logged".into(), + ]))) + } + "query_recent_changes" => { + let data = serde_json::to_string(&self.state.ledger.read()) + .unwrap_or_else(|_| "[]".to_string()); + Ok(ServerResult::from(CallToolResult::text_content(vec![ + data.into(), + ]))) + } + "visualize_graph" => { + let req: VisualizeGraphTool = parse_args(args)?; + let query = req.query.unwrap_or_default().to_lowercase(); + let full = self.state.get_full_graph(); + let mut included = HashSet::new(); + for (name, e) in &full.entities { + if let Some(ns) = &req.namespace + && e.namespace != *ns + { + continue; + } + if query.is_empty() + || name.to_lowercase().contains(&query) + || e.entity_type.to_lowercase().contains(&query) + { + included.insert(name.clone()); + } + } + let mut to_draw = Vec::new(); + for r in &full.relations { + if let Some(ns) = &req.namespace + && r.namespace != *ns + { + continue; + } + if query.is_empty() || included.contains(&r.from) || included.contains(&r.to) { + included.insert(r.from.clone()); + included.insert(r.to.clone()); + to_draw.push(r); + } + } + let mut output = String::from("graph TD;\n"); + for name in &included { + let safe = name.replace("\"", "").replace("(", "").replace(")", ""); + output.push_str(&format!( + " id_{}[\"{}\"];\n", + safe.replace(" ", "_").replace("-", "_").replace(".", "_"), + safe + )); + } + for r in to_draw { + let safe_f = r + .from + .replace(" ", "_") + .replace("-", "_") + .replace(".", "_") + .replace("\"", "") + .replace("(", "") + .replace(")", ""); + let safe_t = + r.to.replace(" ", "_") + .replace("-", "_") + .replace(".", "_") + .replace("\"", "") + .replace("(", "") + .replace(")", ""); + let safe_r = r.relation_type.replace("\"", ""); + output.push_str(&format!( + " id_{}-->|\"{}\"|id_{};\n", + safe_f, safe_r, safe_t + )); + } + if output == "graph TD;\n" { + output = "No nodes found to visualize.".to_string(); + } + Ok(ServerResult::from(CallToolResult::text_content(vec![ + output.into(), + ]))) + } + "add_sticky_note" => { + let req: AddStickyNoteTool = parse_args(args)?; + self.state.sticky.modify(|notes| { + notes.push(StickyNote { + timestamp: SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap() + .as_secs(), + content: req.content, + }); + }); + Ok(ServerResult::from(CallToolResult::text_content(vec![ + "Sticky note added.".into(), + ]))) + } + "read_sticky_notes" => { + let data = serde_json::to_string(&self.state.sticky.read()) + .unwrap_or_else(|_| "[]".to_string()); + Ok(ServerResult::from(CallToolResult::text_content(vec![ + data.into(), + ]))) + } + "condense_entity" => { + let req: CondenseEntityTool = parse_args(args)?; + self.state.apply_sync_write(|master| { + if let Some(e) = master.entities.get_mut(&req.entity_name) { + e.observations = req.summarized_observations; + } + }); + Ok(ServerResult::from(CallToolResult::text_content(vec![ + "Entity condensed".into(), + ]))) + } + "add_task" => { + let req: AddTaskTool = parse_args(args)?; + let now = SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap() + .as_secs(); + let id = uuid::Uuid::new_v4().to_string(); + self.state.tasks.modify(|tasks| { + tasks.push(Task { + id: id.clone(), + title: req.title, + status: "pending".to_string(), + description: req.description, + created_at: now, + updated_at: now, + }); + }); + Ok(ServerResult::from(CallToolResult::text_content(vec![ + format!("Task added with ID: {}", id).into(), + ]))) + } + "update_task_status" => { + let req: UpdateTaskStatusTool = parse_args(args)?; + let mut found = false; + self.state.tasks.modify(|tasks| { + for t in tasks.iter_mut() { + if t.id == req.id { + t.status = req.status.clone(); + t.updated_at = SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap() + .as_secs(); + found = true; + break; + } + } + }); + if found { + Ok(ServerResult::from(CallToolResult::text_content(vec![ + "Task updated.".into(), + ]))) + } else { + Ok(ServerResult::from(CallToolResult::text_content(vec![ + "Task not found.".into(), + ]))) + } + } + "list_active_tasks" => { + let mut tasks = self.state.tasks.read(); + tasks.retain(|t| t.status != "done"); + let data = serde_json::to_string(&tasks).unwrap_or_default(); + Ok(ServerResult::from(CallToolResult::text_content(vec![ + data.into(), + ]))) + } + "store_snippet" => { + let req: StoreSnippetTool = parse_args(args)?; + self.state.snippets.modify(|snippets| { + snippets.retain(|s| s.name != req.name); + snippets.push(Snippet { + name: req.name.clone(), + language: req.language, + code: req.code, + description: req.description, + updated_at: SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap() + .as_secs(), + }); + }); + Ok(ServerResult::from(CallToolResult::text_content(vec![ + format!("Snippet '{}' stored.", req.name).into(), + ]))) + } + "search_snippets" => { + let req: SearchSnippetsTool = parse_args(args)?; + let query = req.query.to_lowercase(); + let snippets = self.state.snippets.read(); + let mut results = Vec::new(); + for s in snippets { + if s.name.to_lowercase().contains(&query) + || s.description.to_lowercase().contains(&query) + || s.language.to_lowercase().contains(&query) + { + results.push(s); + } + } + let data = serde_json::to_string(&results).unwrap_or_default(); + Ok(ServerResult::from(CallToolResult::text_content(vec![ + data.into(), + ]))) + } + "delete_snippet" => { + let req: DeleteSnippetTool = parse_args(args)?; + let mut deleted = false; + self.state.snippets.modify(|snippets| { + let orig = snippets.len(); + snippets.retain(|s| s.name != req.name); + deleted = snippets.len() < orig; + }); + if deleted { + Ok(ServerResult::from(CallToolResult::text_content(vec![ + "Snippet deleted.".into(), + ]))) + } else { + Ok(ServerResult::from(CallToolResult::text_content(vec![ + "Snippet not found.".into(), + ]))) + } + } + "log_decision" => { + let req: LogDecisionTool = parse_args(args)?; + let mut id = String::new(); + self.state.adrs.modify(|adrs| { + id = format!("ADR-{:04}", adrs.len() + 1); + adrs.push(Adr { + id: id.clone(), + title: req.title, + context: req.context, + decision: req.decision, + consequence: req.consequence, + timestamp: SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap() + .as_secs(), + }); + }); + Ok(ServerResult::from(CallToolResult::text_content(vec![ + format!("Decision logged as {}", id).into(), + ]))) + } + "query_decisions" => { + let req: QueryDecisionsTool = parse_args(args)?; + let mut adrs = self.state.adrs.read(); + if let Some(q) = req.query { + let q = q.to_lowercase(); + adrs.retain(|a| { + a.title.to_lowercase().contains(&q) + || a.context.to_lowercase().contains(&q) + || a.decision.to_lowercase().contains(&q) + }); + } + let data = serde_json::to_string(&adrs).unwrap_or_default(); + Ok(ServerResult::from(CallToolResult::text_content(vec![ + data.into(), + ]))) + } + "merge_entities" => { + let req: MergeEntitiesTool = parse_args(args)?; + self.state.apply_sync_write(|master| { + if let Some(src) = master.entities.remove(&req.source_entity) { + if let Some(tgt) = master.entities.get_mut(&req.target_entity) { + tgt.observations.extend(src.observations); + tgt.observations = MemoryState::unique_items(tgt.observations.clone()); + } else { + let mut new_tgt = src.clone(); + new_tgt.name = req.target_entity.clone(); + master.entities.insert(req.target_entity.clone(), new_tgt); + } + } + for r in &mut master.relations { + if r.from == req.source_entity { + r.from = req.target_entity.clone(); + } + if r.to == req.source_entity { + r.to = req.target_entity.clone(); + } + } + master.relations = MemoryState::unique_items(master.relations.clone()); + }); + Ok(ServerResult::from(CallToolResult::text_content(vec![ + "Entities merged".into(), + ]))) + } + "find_orphans" => { + let full = self.state.get_full_graph(); + let mut connected = std::collections::HashSet::new(); + for r in &full.relations { + connected.insert(r.from.clone()); + connected.insert(r.to.clone()); + } + let orphans: Vec = full + .entities + .keys() + .filter(|k| !connected.contains(*k)) + .cloned() + .collect(); + let data = serde_json::to_string(&orphans).unwrap_or_default(); + Ok(ServerResult::from(CallToolResult::text_content(vec![ + data.into(), + ]))) + } + "learn_preference" => { + let req: LearnPreferenceTool = parse_args(args)?; + self.state.prefs.modify(|prefs| { + prefs.insert( + req.key.clone(), + crate::models::Preference { + key: req.key.clone(), + value: req.value, + updated_at: SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap() + .as_secs(), + }, + ); + }); + Ok(ServerResult::from(CallToolResult::text_content(vec![ + "Preference learned".into(), + ]))) + } + "read_preferences" => { + let prefs = self.state.prefs.read(); + let data = serde_json::to_string(&prefs).unwrap_or_default(); + Ok(ServerResult::from(CallToolResult::text_content(vec![ + data.into(), + ]))) + } + "log_error_fix" => { + let req: LogErrorFixTool = parse_args(args)?; + self.state.error_fixes.modify(|fixes| { + fixes.push(crate::models::ErrorFix { + signature: req.signature, + solution: req.solution, + timestamp: SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap() + .as_secs(), + git_commit: req.git_commit, + git_branch: req.git_branch, + }) + }); + Ok(ServerResult::from(CallToolResult::text_content(vec![ + "Error fix logged".into(), + ]))) + } + "search_error_fixes" => { + let req: SearchErrorFixesTool = parse_args(args)?; + let q = req.query.to_lowercase(); + let mut fixes = self.state.error_fixes.read(); + fixes.retain(|f| { + f.signature.to_lowercase().contains(&q) + || f.solution.to_lowercase().contains(&q) + }); + let data = serde_json::to_string(&fixes).unwrap_or_default(); + Ok(ServerResult::from(CallToolResult::text_content(vec![ + data.into(), + ]))) + } + "pin_file" => { + let req: PinFileTool = parse_args(args)?; + self.state.pinned_files.modify(|pinned| { + pinned.retain(|p| { + !(p.namespace == req.namespace && p.file_path == req.file_path) + }); + pinned.push(crate::models::PinnedFile { + namespace: req.namespace, + file_path: req.file_path, + timestamp: SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap() + .as_secs(), + }); + }); + Ok(ServerResult::from(CallToolResult::text_content(vec![ + "File pinned".into(), + ]))) + } + "unpin_file" => { + let req: UnpinFileTool = parse_args(args)?; + self.state.pinned_files.modify(|pinned| { + pinned + .retain(|p| !(p.namespace == req.namespace && p.file_path == req.file_path)) + }); + Ok(ServerResult::from(CallToolResult::text_content(vec![ + "File unpinned".into(), + ]))) + } + "list_pinned_files" => { + let req: ListPinnedFilesTool = parse_args(args)?; + let mut pinned = self.state.pinned_files.read(); + if let Some(ns) = req.namespace { + pinned.retain(|p| p.namespace == ns); + } + let data = serde_json::to_string(&pinned).unwrap_or_default(); + Ok(ServerResult::from(CallToolResult::text_content(vec![ + data.into(), + ]))) + } + "add_session_summary" => { + let req: AddSessionSummaryTool = parse_args(args)?; + self.state.session_summaries.modify(|summaries| { + summaries.push(crate::models::SessionSummary { + summary: req.summary, + namespace: req.namespace, + timestamp: SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap() + .as_secs(), + }) + }); + Ok(ServerResult::from(CallToolResult::text_content(vec![ + "Session summary added".into(), + ]))) + } + "get_project_timeline" => { + let req: GetProjectTimelineTool = parse_args(args)?; + let mut summaries = self.state.session_summaries.read(); + if let Some(ns) = req.namespace { + summaries.retain(|s| s.namespace == ns); + } + summaries.sort_by_key(|s| s.timestamp); + let data = serde_json::to_string(&summaries).unwrap_or_default(); + Ok(ServerResult::from(CallToolResult::text_content(vec![ + data.into(), + ]))) + } + "leave_handoff_memo" => { + let req: LeaveHandoffMemoTool = parse_args(args)?; + self.state.handoff_memos.modify(|memos| { + memos.push(crate::models::HandoffMemo { + id: uuid::Uuid::new_v4().to_string(), + author: "agy".to_string(), + content: req.content, + namespace: req.namespace, + timestamp: SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap() + .as_secs(), + }) + }); + Ok(ServerResult::from(CallToolResult::text_content(vec![ + "Handoff memo left".into(), + ]))) + } + "read_handoff_memos" => { + let req: ReadHandoffMemosTool = parse_args(args)?; + let mut memos = self.state.handoff_memos.read(); + if let Some(ns) = req.namespace { + memos.retain(|m| m.namespace == ns); + } + let data = serde_json::to_string(&memos).unwrap_or_default(); + Ok(ServerResult::from(CallToolResult::text_content(vec![ + data.into(), + ]))) + } + "clear_handoff_memos" => { + let req: ClearHandoffMemosTool = parse_args(args)?; + let ids: HashSet<_> = req.ids.into_iter().collect(); + self.state + .handoff_memos + .modify(|memos| memos.retain(|m| !ids.contains(&m.id))); + Ok(ServerResult::from(CallToolResult::text_content(vec![ + "Handoff memos cleared".into(), + ]))) + } + "update_env_fingerprint" => { + let req: UpdateEnvFingerprintTool = parse_args(args)?; + self.state.env_fingerprints.modify(|fps| { + fps.insert( + req.namespace.clone(), + crate::models::EnvFingerprint { + namespace: req.namespace.clone(), + os: std::env::consts::OS.to_string(), + shell: std::env::var("SHELL").unwrap_or_else(|_| "unknown".to_string()), + tool_versions: req.tool_versions, + updated_at: SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap() + .as_secs(), + }, + ); + }); + Ok(ServerResult::from(CallToolResult::text_content(vec![ + "Env fingerprint updated".into(), + ]))) + } + "read_env_fingerprint" => { + let req: ReadEnvFingerprintTool = parse_args(args)?; + let fps = self.state.env_fingerprints.read(); + if let Some(fp) = fps.get(&req.namespace) { + let data = serde_json::to_string(fp).unwrap_or_default(); + Ok(ServerResult::from(CallToolResult::text_content(vec![ + data.into(), + ]))) + } else { + Ok(ServerResult::from(CallToolResult::text_content(vec![ + "{}".into(), + ]))) + } + } + "log_env_requirement" => { + let req: LogEnvRequirementTool = parse_args(args)?; + self.state.env_requirements.modify(|reqs| { + reqs.retain(|r| !(r.namespace == req.namespace && r.key == req.key)); + reqs.push(crate::models::EnvRequirement { + namespace: req.namespace, + key: req.key, + description: req.description, + is_secret: req.is_secret, + }); + }); + Ok(ServerResult::from(CallToolResult::text_content(vec![ + "Env requirement logged".into(), + ]))) + } + "add_milestone" => { + let req: AddMilestoneTool = parse_args(args)?; + self.state.milestones.modify(|ms| { + ms.push(crate::models::Milestone { + id: uuid::Uuid::new_v4().to_string(), + title: req.title, + status: "pending".to_string(), + namespace: req.namespace, + target_date: None, + }) + }); + Ok(ServerResult::from(CallToolResult::text_content(vec![ + "Milestone added".into(), + ]))) + } + "update_milestone" => { + let req: UpdateMilestoneTool = parse_args(args)?; + let mut found = false; + self.state.milestones.modify(|ms| { + for m in ms.iter_mut() { + if m.id == req.id { + m.status = req.status.clone(); + found = true; + break; + } + } + }); + if found { + Ok(ServerResult::from(CallToolResult::text_content(vec![ + "Milestone updated".into(), + ]))) + } else { + Ok(ServerResult::from(CallToolResult::text_content(vec![ + "Milestone not found".into(), + ]))) + } + } + "list_milestones" => { + let req: ListMilestonesTool = parse_args(args)?; + let mut ms = self.state.milestones.read(); + if let Some(ns) = req.namespace { + ms.retain(|m| m.namespace == ns); + } + let data = serde_json::to_string(&ms).unwrap_or_default(); + Ok(ServerResult::from(CallToolResult::text_content(vec![ + data.into(), + ]))) + } + "generate_standup_report" => { + let req: GenerateStandupReportTool = parse_args(args)?; + let cutoff = SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap() + .as_secs() + .saturating_sub(req.hours_lookback * 3600); + let tasks = self + .state + .tasks + .read() + .into_iter() + .filter(|t| t.updated_at >= cutoff) + .collect::>(); + let changes = self + .state + .ledger + .read() + .into_iter() + .filter(|c| c.timestamp >= cutoff) + .collect::>(); + let summaries = self + .state + .session_summaries + .read() + .into_iter() + .filter(|s| s.namespace == req.namespace && s.timestamp >= cutoff) + .collect::>(); + let report = serde_json::json!({ "tasks_updated": tasks, "code_changes": changes, "session_summaries": summaries }); + Ok(ServerResult::from(CallToolResult::text_content(vec![ + report.to_string().into(), + ]))) + } + "register_environment" => { + let req: RegisterEnvironmentTool = parse_args(args)?; + self.state.environments.modify(|envs| { + envs.retain(|e| !(e.namespace == req.namespace && e.name == req.name)); + envs.push(crate::models::EnvironmentDetail { + namespace: req.namespace, + name: req.name, + url: req.url, + description: req.description, + requires_vpn: req.requires_vpn, + updated_at: SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap() + .as_secs(), + }); + }); + Ok(ServerResult::from(CallToolResult::text_content(vec![ + "Environment registered".into(), + ]))) + } + "get_environment_details" => { + let req: GetEnvironmentDetailsTool = parse_args(args)?; + let mut envs = self.state.environments.read(); + envs.retain(|e| e.namespace == req.namespace); + let data = serde_json::to_string(&envs).unwrap_or_default(); + Ok(ServerResult::from(CallToolResult::text_content(vec![ + data.into(), + ]))) + } + "add_pr_checklist_item" => { + let req: AddPrChecklistItemTool = parse_args(args)?; + self.state.pr_checklists.modify(|items| { + items.push(crate::models::PrChecklistItem { + namespace: req.namespace, + id: uuid::Uuid::new_v4().to_string(), + description: req.description, + }) + }); + Ok(ServerResult::from(CallToolResult::text_content(vec![ + "PR checklist item added".into(), + ]))) + } + "get_pr_checklist" => { + let req: GetPrChecklistTool = parse_args(args)?; + let mut items = self.state.pr_checklists.read(); + items.retain(|i| i.namespace == req.namespace); + let data = serde_json::to_string(&items).unwrap_or_default(); + Ok(ServerResult::from(CallToolResult::text_content(vec![ + data.into(), + ]))) + } + "clear_pr_checklist" => { + let req: ClearPrChecklistTool = parse_args(args)?; + self.state + .pr_checklists + .modify(|items| items.retain(|i| i.namespace != req.namespace)); + Ok(ServerResult::from(CallToolResult::text_content(vec![ + "PR checklist cleared".into(), + ]))) + } + "log_tech_debt" => { + let req: LogTechDebtTool = parse_args(args)?; + self.state.tech_debts.modify(|debts| { + debts.push(crate::models::TechDebt { + id: uuid::Uuid::new_v4().to_string(), + namespace: req.namespace, + description: req.description, + ideal_solution: req.ideal_solution, + is_resolved: false, + created_at: SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap() + .as_secs(), + git_commit: req.git_commit, + git_branch: req.git_branch, + }) + }); + Ok(ServerResult::from(CallToolResult::text_content(vec![ + "Tech debt logged".into(), + ]))) + } + "resolve_tech_debt" => { + let req: ResolveTechDebtTool = parse_args(args)?; + let mut found = false; + self.state.tech_debts.modify(|debts| { + for d in debts.iter_mut() { + if d.id == req.id { + d.is_resolved = true; + found = true; + break; + } + } + }); + if found { + Ok(ServerResult::from(CallToolResult::text_content(vec![ + "Tech debt resolved".into(), + ]))) + } else { + Ok(ServerResult::from(CallToolResult::text_content(vec![ + "Tech debt not found".into(), + ]))) + } + } + "list_tech_debt" => { + let req: ListTechDebtTool = parse_args(args)?; + let mut debts = self.state.tech_debts.read(); + debts.retain(|d| { + d.namespace == req.namespace && (req.include_resolved || !d.is_resolved) + }); + let data = serde_json::to_string(&debts).unwrap_or_default(); + Ok(ServerResult::from(CallToolResult::text_content(vec![ + data.into(), + ]))) + } + "save_context_workspace" => { + let req: SaveContextWorkspaceTool = parse_args(args)?; + self.state.context_workspaces.modify(|ws| { + ws.retain(|w| !(w.namespace == req.namespace && w.name == req.name)); + ws.push(crate::models::ContextWorkspace { + namespace: req.namespace, + name: req.name, + pinned_files: req.pinned_files, + active_task_ids: req.active_task_ids, + saved_at: SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap() + .as_secs(), + }); + }); + Ok(ServerResult::from(CallToolResult::text_content(vec![ + "Context workspace saved".into(), + ]))) + } + "load_context_workspace" => { + let req: LoadContextWorkspaceTool = parse_args(args)?; + let mut ws = self.state.context_workspaces.read(); + ws.retain(|w| w.namespace == req.namespace && w.name == req.name); + let data = serde_json::to_string(&ws.first()).unwrap_or_default(); + Ok(ServerResult::from(CallToolResult::text_content(vec![ + data.into(), + ]))) + } + "list_context_workspaces" => { + let req: ListContextWorkspacesTool = parse_args(args)?; + let mut ws = self.state.context_workspaces.read(); + ws.retain(|w| w.namespace == req.namespace); + let data = serde_json::to_string(&ws).unwrap_or_default(); + Ok(ServerResult::from(CallToolResult::text_content(vec![ + data.into(), + ]))) + } + "omni_search" => { + let req: OmniSearchTool = parse_args(args)?; + let q = req.query.to_lowercase(); + + let mut kg = self.state.get_full_graph(); + kg.entities.retain(|name, e| { + (req.namespace.is_none() || e.namespace == *req.namespace.as_ref().unwrap()) + && (name.to_lowercase().contains(&q) + || e.entity_type.to_lowercase().contains(&q) + || e.observations.join(" ").to_lowercase().contains(&q)) + }); + + let tasks: Vec<_> = self + .state + .tasks + .read() + .into_iter() + .filter(|t| { + t.title.to_lowercase().contains(&q) + || t.description.to_lowercase().contains(&q) + }) + .collect(); + let snippets: Vec<_> = self + .state + .snippets + .read() + .into_iter() + .filter(|s| { + s.name.to_lowercase().contains(&q) + || s.description.to_lowercase().contains(&q) + }) + .collect(); + let adrs: Vec<_> = self + .state + .adrs + .read() + .into_iter() + .filter(|a| { + a.title.to_lowercase().contains(&q) + || a.context.to_lowercase().contains(&q) + || a.decision.to_lowercase().contains(&q) + }) + .collect(); + let tech_debts: Vec<_> = self + .state + .tech_debts + .read() + .into_iter() + .filter(|d| { + (req.namespace.is_none() || d.namespace == *req.namespace.as_ref().unwrap()) + && (d.description.to_lowercase().contains(&q) + || d.ideal_solution.to_lowercase().contains(&q)) + }) + .collect(); + let memos: Vec<_> = self + .state + .handoff_memos + .read() + .into_iter() + .filter(|m| { + (req.namespace.is_none() || m.namespace == *req.namespace.as_ref().unwrap()) + && m.content.to_lowercase().contains(&q) + }) + .collect(); + let error_fixes: Vec<_> = self + .state + .error_fixes + .read() + .into_iter() + .filter(|f| { + f.signature.to_lowercase().contains(&q) + || f.solution.to_lowercase().contains(&q) + }) + .collect(); + + let report = serde_json::json!({ + "knowledge_graph": kg.entities, + "tasks": tasks, + "snippets": snippets, + "adrs": adrs, + "tech_debts": tech_debts, + "handoff_memos": memos, + "error_fixes": error_fixes + }); + Ok(ServerResult::from(CallToolResult::text_content(vec![ + report.to_string().into(), + ]))) + } + "get_project_health" => { + let req: GetProjectHealthTool = parse_args(args)?; + let active_tasks = self + .state + .tasks + .read() + .into_iter() + .filter(|t| t.status != "done") + .count(); + let unresolved_debt = self + .state + .tech_debts + .read() + .into_iter() + .filter(|d| d.namespace == req.namespace && !d.is_resolved) + .count(); + let unread_memos = self + .state + .handoff_memos + .read() + .into_iter() + .filter(|m| m.namespace == req.namespace) + .count(); + let active_milestones = self + .state + .milestones + .read() + .into_iter() + .filter(|m| m.namespace == req.namespace && m.status != "done") + .count(); + let remaining_checklists = self + .state + .pr_checklists + .read() + .into_iter() + .filter(|c| c.namespace == req.namespace) + .count(); + + let report = serde_json::json!({ + "active_tasks": active_tasks, + "unresolved_tech_debt": unresolved_debt, + "unread_handoff_memos": unread_memos, + "active_milestones": active_milestones, + "remaining_pr_checklist_items": remaining_checklists + }); + Ok(ServerResult::from(CallToolResult::text_content(vec![ + report.to_string().into(), + ]))) + } + _ => Err(CallToolError::unknown_tool(params.name)), + } + } +} + + diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..ec2e9ec --- /dev/null +++ b/src/main.rs @@ -0,0 +1,257 @@ +mod handlers; +mod models; +mod state; +mod store; +mod tools; + +use crate::handlers::MemoryHandler; +use crate::models::*; +use crate::state::MemoryState; +use crate::store::Store; +use rust_mcp_sdk::{ + McpServer, ServerDetails, StdioTransport, ToMcpServerHandler, TransportOptions, + error::SdkResult, + mcp_server::{McpServerOptions, server_runtime}, + schema::{Implementation, ServerCapabilities, ServerCapabilitiesTools}, +}; +use std::fs; +use std::path::PathBuf; +use std::sync::{Arc, RwLock}; +use std::time::{Duration, SystemTime, UNIX_EPOCH}; +use tokio::time::sleep; + +use clap::{Parser, Subcommand}; +use std::collections::HashMap; + +#[derive(Parser)] +#[command(author, version, about, long_about = None)] +struct Cli { + #[command(subcommand)] + command: Option, +} + +#[derive(Subcommand)] +enum Commands { + Gate { + #[command(subcommand)] + subcmd: GateCommands, + }, +} + +#[derive(Subcommand)] +enum GateCommands { + Set { + #[arg(long)] + action: String, + #[arg(long)] + target: String, + #[arg(long)] + namespace: Option, + #[arg(short = 'p', long = "param")] + params: Vec, + #[arg(long, conflicts_with = "block")] + authorize: bool, + #[arg(long, conflicts_with = "authorize")] + block: bool, + #[arg(long)] + reason: Option, + }, + Verify { + #[arg(long)] + action: String, + #[arg(long)] + target: String, + #[arg(long)] + namespace: Option, + #[arg(short = 'p', long = "param")] + params: Vec, + #[arg(long)] + consume: bool, + }, +} + + +async fn reconcile_worker(state: Arc) { + loop { + sleep(Duration::from_secs(5)).await; + let pattern = format!("{}/delta_*.json", state.base_dir.display()); + let has_local = { + let session = state.session_graph.read().unwrap(); + !session.entities.is_empty() || !session.relations.is_empty() + }; + let has_files = glob::glob(&pattern).map(|p| p.count() > 0).unwrap_or(false); + if has_local || has_files { + state.apply_sync_write(|_master| {}); + } + + let now = SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap() + .as_secs(); + state.ledger.modify(|ledger| { + let seven_days = now.saturating_sub(7 * 24 * 60 * 60); + ledger.retain(|c| c.timestamp >= seven_days); + if ledger.len() > 1000 { + let excess = ledger.len() - 1000; + ledger.drain(0..excess); + } + }); + state.sticky.modify(|notes| { + notes.retain(|note| note.timestamp >= now.saturating_sub(24 * 60 * 60)); + }); + } +} + + +fn run_server(state: Arc) -> SdkResult<()> { + let rt = tokio::runtime::Runtime::new().unwrap(); + rt.block_on(async { + let state_clone = state.clone(); + tokio::spawn(async move { + reconcile_worker(state_clone).await; + }); + + let server_details = ServerDetails { + server_info: Implementation { + name: "gemini-mcp-memory".into(), + version: "3.0.0".into(), + title: Some("MCP Memory Server".into()), + description: Some("Persistent memory MCP server".into()), + icons: vec![], + website_url: None, + }, + capabilities: ServerCapabilities { + tools: Some(ServerCapabilitiesTools { + list_changed: Some(true), + }), + ..Default::default() + }, + instructions: None, + meta: None, + }; + let server = server_runtime::create_server(McpServerOptions { + transport: StdioTransport::new(TransportOptions::default())?, + handler: MemoryHandler { state }.to_mcp_server_handler(), + server_details, + message_observer: None, + }); + server.start().await + }) +} + +fn main() -> SdkResult<()> { + let cli = Cli::parse(); + + let base_dir = std::env::var("MCP_MEMORY_STORE_DIR").unwrap_or_else(|_| { + dirs::home_dir() + .map(|mut h| { + h.push(".gemini/mcp_memory"); + h.to_string_lossy().into_owned() + }) + .unwrap_or_else(|| ".gemini/mcp_memory".into()) + }); + let base = PathBuf::from(base_dir); + fs::create_dir_all(&base).expect("Failed to create store dir"); + + let state = Arc::new(MemoryState { + master_path: base.join("knowledge_graph_master.json"), + delta_path: base.join(format!("delta_{}.json", uuid::Uuid::new_v4())), + session_graph: RwLock::new(KnowledgeGraph::default()), + base_dir: base.clone(), + master_cache: RwLock::new((KnowledgeGraph::default(), SystemTime::UNIX_EPOCH)), + ledger: Store::new(base.join("audit_ledger.json")), + sticky: Store::new(base.join("sticky_notes.json")), + tasks: Store::new(base.join("tasks.json")), + snippets: Store::new(base.join("snippets.json")), + adrs: Store::new(base.join("adrs.json")), + prefs: Store::new(base.join("preferences.json")), + error_fixes: Store::new(base.join("error_fixes.json")), + pinned_files: Store::new(base.join("pinned_files.json")), + session_summaries: Store::new(base.join("session_summaries.json")), + handoff_memos: Store::new(base.join("handoff_memos.json")), + env_fingerprints: Store::new(base.join("env_fingerprints.json")), + env_requirements: Store::new(base.join("env_requirements.json")), + milestones: Store::new(base.join("milestones.json")), + environments: Store::new(base.join("environments.json")), + pr_checklists: Store::new(base.join("pr_checklists.json")), + tech_debts: Store::new(base.join("tech_debts.json")), + gates: Store::new(base.join("gates.json")), + context_workspaces: Store::new(base.join("context_workspaces.json")), + }); + + if let Some(command) = cli.command { + match command { + Commands::Gate { subcmd } => { + match subcmd { + GateCommands::Set { action, target, namespace, params, authorize, block, reason } => { + let status = if authorize { "authorized".to_string() } else if block { "blocked".to_string() } else { "pending".to_string() }; + let mut param_map = HashMap::new(); + for p in params { + if let Some((k, v)) = p.split_once('=') { + param_map.insert(k.to_string(), v.to_string()); + } + } + let record = GateRecord { + id: uuid::Uuid::new_v4().to_string(), + action: action.clone(), + target: target.clone(), + namespace, + params: param_map, + status, + reason, + timestamp: SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_secs(), + }; + state.gates.modify(|gates| { + gates.retain(|g| !(g.action == record.action && g.target == record.target)); + gates.push(record); + }); + println!("Gate state updated."); + std::process::exit(0); + } + GateCommands::Verify { action, target, namespace, params, consume } => { + let mut param_map = HashMap::new(); + for p in params { + if let Some((k, v)) = p.split_once('=') { + param_map.insert(k.to_string(), v.to_string()); + } + } + let mut found = None; + let mut to_remove = None; + state.gates.modify(|gates| { + if let Some(idx) = gates.iter().position(|g| g.action == action && g.target == target && g.namespace == namespace && g.params == param_map) { + found = Some(gates[idx].clone()); + if consume { + to_remove = Some(idx); + } + } + if let Some(idx) = to_remove { + gates.remove(idx); + } + }); + + match found { + Some(record) => { + if record.status == "authorized" { + std::process::exit(0); + } else { + if let Some(r) = record.reason { + eprintln!("❌ Action blocked. Reason: {}", r); + } else { + eprintln!("❌ Action blocked."); + } + std::process::exit(1); + } + } + None => { + eprintln!("❌ Action not yet authorized (no gate record found)."); + std::process::exit(2); + } + } + } + } + } + } + } + + run_server(state) +} diff --git a/src/models.rs b/src/models.rs new file mode 100644 index 0000000..e40145b --- /dev/null +++ b/src/models.rs @@ -0,0 +1,175 @@ +use serde::{Deserialize, Serialize}; +use std::collections::HashMap; + +#[derive(Debug, Clone, Serialize, Deserialize, Default)] +pub struct CodeChange { + pub timestamp: u64, + pub file_path: String, + pub description: String, + pub git_commit: Option, + pub git_branch: Option, +} +#[derive(Debug, Clone, Serialize, Deserialize, Default)] +pub struct StickyNote { + pub timestamp: u64, + pub content: String, +} +pub fn default_namespace() -> String { + "global".to_string() +} +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Entity { + pub name: String, + #[serde(rename = "entityType")] + pub entity_type: String, + #[serde(default)] + pub observations: Vec, + #[serde(default = "default_namespace")] + pub namespace: String, +} +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)] +pub struct Relation { + pub from: String, + pub to: String, + #[serde(rename = "relationType")] + pub relation_type: String, + #[serde(default = "default_namespace")] + pub namespace: String, +} +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +pub struct KnowledgeGraph { + #[serde(default)] + pub entities: HashMap, + #[serde(default)] + pub relations: Vec, +} +#[derive(Debug, Clone, Serialize, Deserialize, Default)] +pub struct Task { + pub id: String, + pub title: String, + pub status: String, + pub description: String, + pub created_at: u64, + pub updated_at: u64, +} +#[derive(Debug, Clone, Serialize, Deserialize, Default)] +pub struct Snippet { + pub name: String, + pub language: String, + pub code: String, + pub description: String, + pub updated_at: u64, +} +#[derive(Debug, Clone, Serialize, Deserialize, Default)] +pub struct Adr { + pub id: String, + pub title: String, + pub context: String, + pub decision: String, + pub consequence: String, + pub timestamp: u64, +} +#[derive(Debug, Clone, Serialize, Deserialize, Default)] +pub struct Preference { + pub key: String, + pub value: String, + pub updated_at: u64, +} +#[derive(Debug, Clone, Serialize, Deserialize, Default)] +pub struct ErrorFix { + pub signature: String, + pub solution: String, + pub timestamp: u64, + pub git_commit: Option, + pub git_branch: Option, +} +#[derive(Debug, Clone, Serialize, Deserialize, Default)] +pub struct PinnedFile { + pub namespace: String, + pub file_path: String, + pub timestamp: u64, +} +#[derive(Debug, Clone, Serialize, Deserialize, Default)] +pub struct SessionSummary { + pub timestamp: u64, + pub summary: String, + pub namespace: String, +} +#[derive(Debug, Clone, Serialize, Deserialize, Default)] +pub struct HandoffMemo { + pub id: String, + pub author: String, + pub content: String, + pub namespace: String, + pub timestamp: u64, +} +#[derive(Debug, Clone, Serialize, Deserialize, Default)] +pub struct EnvFingerprint { + pub namespace: String, + pub os: String, + pub shell: String, + pub tool_versions: HashMap, + pub updated_at: u64, +} +#[derive(Debug, Clone, Serialize, Deserialize, Default)] +pub struct EnvRequirement { + pub namespace: String, + pub key: String, + pub description: String, + pub is_secret: bool, +} +#[derive(Debug, Clone, Serialize, Deserialize, Default)] +pub struct Milestone { + pub id: String, + pub title: String, + pub status: String, + pub namespace: String, + pub target_date: Option, +} +#[derive(Debug, Clone, Serialize, Deserialize, Default)] +pub struct EnvironmentDetail { + pub namespace: String, + pub name: String, + pub url: String, + pub description: String, + pub requires_vpn: bool, + pub updated_at: u64, +} +#[derive(Debug, Clone, Serialize, Deserialize, Default)] +pub struct PrChecklistItem { + pub namespace: String, + pub id: String, + pub description: String, +} +#[derive(Debug, Clone, Serialize, Deserialize, Default)] +pub struct TechDebt { + pub id: String, + pub namespace: String, + pub description: String, + pub ideal_solution: String, + pub is_resolved: bool, + pub created_at: u64, + pub git_commit: Option, + pub git_branch: Option, +} +#[derive(Debug, Clone, Serialize, Deserialize, Default)] +pub struct ContextWorkspace { + pub name: String, + pub namespace: String, + pub pinned_files: Vec, + pub active_task_ids: Vec, + pub saved_at: u64, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[derive(Default)] +pub struct GateRecord { + pub id: String, + pub action: String, + pub target: String, + pub namespace: Option, + pub params: std::collections::HashMap, + pub status: String, + pub reason: Option, + pub timestamp: u64, +} diff --git a/src/state.rs b/src/state.rs new file mode 100644 index 0000000..1c6873d --- /dev/null +++ b/src/state.rs @@ -0,0 +1,141 @@ +use crate::models::*; +use crate::store::{Store, read_json_file, write_json_atomic}; +use std::collections::{HashMap, HashSet}; +use std::fs; +use std::path::PathBuf; +use std::sync::RwLock; +use std::time::{Duration, SystemTime}; + +pub struct MemoryState { + pub base_dir: PathBuf, + pub master_path: PathBuf, + pub delta_path: PathBuf, + pub session_graph: RwLock, + pub master_cache: RwLock<(KnowledgeGraph, SystemTime)>, + pub ledger: Store>, + pub sticky: Store>, + pub tasks: Store>, + pub snippets: Store>, + pub adrs: Store>, + pub prefs: Store>, + pub error_fixes: Store>, + pub pinned_files: Store>, + pub session_summaries: Store>, + pub handoff_memos: Store>, + pub env_fingerprints: Store>, + pub env_requirements: Store>, + pub milestones: Store>, + pub environments: Store>, + pub pr_checklists: Store>, + pub tech_debts: Store>, + pub gates: Store>, + pub context_workspaces: Store>, +} + +impl MemoryState { + fn master_mtime(&self) -> SystemTime { + fs::metadata(&self.master_path) + .and_then(|m| m.modified()) + .unwrap_or(SystemTime::UNIX_EPOCH) + } + + pub fn unique_items(input: Vec) -> Vec { + let mut keys = HashSet::new(); + let mut list = Vec::new(); + for entry in input { + if keys.insert(entry.clone()) { + list.push(entry); + } + } + list + } + pub fn merge_graphs(dest: &mut KnowledgeGraph, src: &KnowledgeGraph) { + for (name, src_ent) in &src.entities { + let dest_ent = dest + .entities + .entry(name.clone()) + .or_insert_with(|| src_ent.clone()); + if dest_ent.name == src_ent.name { + dest_ent.observations.extend(src_ent.observations.clone()); + dest_ent.observations = Self::unique_items(dest_ent.observations.clone()); + } + } + dest.relations.extend(src.relations.clone()); + dest.relations = Self::unique_items(dest.relations.clone()); + } + + pub fn read_master_cached(&self) -> KnowledgeGraph { + let current_mtime = self.master_mtime(); + { + let lock = self.master_cache.read().unwrap(); + if lock.1 == current_mtime { + return lock.0.clone(); + } + } + let mut lock = self.master_cache.write().unwrap(); + let new_mtime = self.master_mtime(); + if lock.1 != new_mtime { + lock.0 = read_json_file(&self.master_path); + lock.1 = new_mtime; + } + lock.0.clone() + } + + pub fn get_full_graph(&self) -> KnowledgeGraph { + let mut master = self.read_master_cached(); + let pattern = format!("{}/delta_*.json", self.base_dir.display()); + if let Ok(paths) = glob::glob(&pattern) { + for path in paths.flatten() { + if path == self.delta_path { + let session_graph = self.session_graph.read().unwrap(); + Self::merge_graphs(&mut master, &session_graph); + } else { + let d: KnowledgeGraph = read_json_file(&path); + Self::merge_graphs(&mut master, &d); + } + } + } + master + } + pub fn write_to_local_delta(&self, update_fn: F) { + let mut session_graph = self.session_graph.write().unwrap(); + update_fn(&mut session_graph); + let _ = write_json_atomic(&self.delta_path, &*session_graph); + } + pub fn apply_sync_write(&self, update_fn: F) { + let lock_path = self.base_dir.join("master.lock"); + let mut attempts = 0; + loop { + if fs::OpenOptions::new() + .create_new(true) + .write(true) + .open(&lock_path) + .is_ok() + { + break; + } + if attempts > 100 { + let _ = fs::remove_file(&lock_path); + } + attempts += 1; + std::thread::sleep(Duration::from_millis(50)); + } + let mut master = self.get_full_graph(); + let pattern = format!("{}/delta_*.json", self.base_dir.display()); + if let Ok(paths) = glob::glob(&pattern) { + for path in paths.flatten() { + let _ = fs::remove_file(&path); + } + } + *self.session_graph.write().unwrap() = KnowledgeGraph::default(); + update_fn(&mut master); + let _ = write_json_atomic(&self.master_path, &master); + + let mut cache_lock = self.master_cache.write().unwrap(); + cache_lock.0 = master; + cache_lock.1 = self.master_mtime(); + + let _ = fs::remove_file(&lock_path); + } +} + diff --git a/src/store.rs b/src/store.rs new file mode 100644 index 0000000..0dd114a --- /dev/null +++ b/src/store.rs @@ -0,0 +1,82 @@ +use serde::{de::DeserializeOwned, Serialize}; +use std::fs; +use std::path::{Path, PathBuf}; +use std::sync::RwLock; +use std::time::SystemTime; + +pub fn read_json_file(path: &Path) -> T { + if let Ok(data) = fs::read(path) + && let Ok(parsed) = serde_json::from_slice(&data) { + return parsed; + } + let bak_path = path.with_extension("json.bak"); + if let Ok(data) = fs::read(&bak_path) + && let Ok(parsed) = serde_json::from_slice(&data) { + let _ = fs::write(path, data); + return parsed; + } + T::default() +} + +pub fn write_json_atomic(path: &Path, data: &T) -> std::io::Result<()> { + if path.exists() { + let bak_path = path.with_extension("json.bak"); + let _ = fs::copy(path, &bak_path); + } + let tmp_path = path.with_extension("json.tmp"); + let json_data = serde_json::to_string_pretty(data)?; + fs::write(&tmp_path, json_data)?; + fs::rename(&tmp_path, path) +} + +pub struct Store { + pub path: PathBuf, + pub cache: RwLock<(T, SystemTime)>, +} + +impl Store { + fn mtime(&self) -> SystemTime { + fs::metadata(&self.path) + .and_then(|m| m.modified()) + .unwrap_or(SystemTime::UNIX_EPOCH) + } + + pub fn new(path: PathBuf) -> Self { + let mtime = fs::metadata(&path) + .and_then(|m| m.modified()) + .unwrap_or(SystemTime::UNIX_EPOCH); + let initial_data = read_json_file(&path); + Self { + path, + cache: RwLock::new((initial_data, mtime)), + } + } + + pub fn read(&self) -> T { + let current_mtime = self.mtime(); + { + let lock = self.cache.read().unwrap(); + if lock.1 == current_mtime { + return lock.0.clone(); + } + } + let mut lock = self.cache.write().unwrap(); + let new_mtime = self.mtime(); + if lock.1 != new_mtime { + lock.0 = read_json_file(&self.path); + lock.1 = new_mtime; + } + lock.0.clone() + } + + pub fn modify(&self, f: F) { + let mut lock = self.cache.write().unwrap(); + let current_mtime = self.mtime(); + if lock.1 != current_mtime { + lock.0 = read_json_file(&self.path); + } + f(&mut lock.0); + let _ = write_json_atomic(&self.path, &lock.0); + lock.1 = self.mtime(); + } +} diff --git a/src/tools.rs b/src/tools.rs new file mode 100644 index 0000000..cfc74a6 --- /dev/null +++ b/src/tools.rs @@ -0,0 +1,364 @@ +use rust_mcp_sdk::macros; +use serde::{Deserialize, Serialize}; + +#[macros::mcp_tool(name = "create_entities", description = "Create multiple new entities")] +#[derive(Debug, Deserialize, Serialize, macros::JsonSchema)] +pub struct CreateEntitiesTool { + pub entities: Vec, +} +#[macros::mcp_tool( + name = "create_relations", + description = "Create multiple new relations" +)] +#[derive(Debug, Deserialize, Serialize, macros::JsonSchema)] +pub struct CreateRelationsTool { + pub relations: Vec, +} +#[macros::mcp_tool( + name = "add_observations", + description = "Add observations to entities" +)] +#[derive(Debug, Deserialize, Serialize, macros::JsonSchema)] +pub struct AddObservationsTool { + pub observations: Vec, +} +#[macros::mcp_tool( + name = "delete_entities", + description = "Delete entities and relations" +)] +#[derive(Debug, Deserialize, Serialize, macros::JsonSchema)] +pub struct DeleteEntitiesTool { + #[serde(rename = "entityNames")] + pub entity_names: Vec, +} +#[macros::mcp_tool(name = "delete_observations", description = "Delete observations")] +#[derive(Debug, Deserialize, Serialize, macros::JsonSchema)] +pub struct DeleteObservationsTool { + pub deletions: Vec, +} +#[macros::mcp_tool(name = "delete_relations", description = "Delete relations")] +#[derive(Debug, Deserialize, Serialize, macros::JsonSchema)] +pub struct DeleteRelationsTool { + pub relations: Vec, +} +#[macros::mcp_tool(name = "read_graph", description = "Read the entire knowledge graph")] +#[derive(Debug, Deserialize, Serialize, macros::JsonSchema)] +pub struct ReadGraphTool { + pub namespace: Option, +} +#[macros::mcp_tool(name = "search_nodes", description = "Search for nodes")] +#[derive(Debug, Deserialize, Serialize, macros::JsonSchema)] +pub struct SearchNodesTool { + pub query: String, + pub namespace: Option, +} +#[macros::mcp_tool(name = "open_nodes", description = "Open specific nodes")] +#[derive(Debug, Deserialize, Serialize, macros::JsonSchema)] +pub struct OpenNodesTool { + pub names: Vec, +} +#[macros::mcp_tool(name = "log_code_change", description = "Log a code change")] +#[derive(Debug, Deserialize, Serialize, macros::JsonSchema)] +pub struct LogCodeChangeTool { + #[serde(rename = "filePath")] + pub file_path: String, + pub description: String, + pub git_commit: Option, + pub git_branch: Option, +} +#[macros::mcp_tool(name = "query_recent_changes", description = "Query the audit ledger")] +#[derive(Debug, Deserialize, Serialize, macros::JsonSchema)] +pub struct QueryRecentChangesTool {} +#[macros::mcp_tool(name = "visualize_graph", description = "Generate Mermaid diagram")] +#[derive(Debug, Deserialize, Serialize, macros::JsonSchema)] +pub struct VisualizeGraphTool { + pub query: Option, + pub namespace: Option, +} +#[macros::mcp_tool(name = "add_sticky_note", description = "Add a sticky note")] +#[derive(Debug, Deserialize, Serialize, macros::JsonSchema)] +pub struct AddStickyNoteTool { + pub content: String, +} +#[macros::mcp_tool(name = "read_sticky_notes", description = "Read sticky notes")] +#[derive(Debug, Deserialize, Serialize, macros::JsonSchema)] +pub struct ReadStickyNotesTool {} +#[macros::mcp_tool(name = "condense_entity", description = "Condense observations")] +#[derive(Debug, Deserialize, Serialize, macros::JsonSchema)] +pub struct CondenseEntityTool { + #[serde(rename = "entityName")] + pub entity_name: String, + pub summarized_observations: Vec, +} +#[macros::mcp_tool(name = "add_task", description = "Add a task")] +#[derive(Debug, Deserialize, Serialize, macros::JsonSchema)] +pub struct AddTaskTool { + pub title: String, + pub description: String, +} +#[macros::mcp_tool(name = "update_task_status", description = "Update task status")] +#[derive(Debug, Deserialize, Serialize, macros::JsonSchema)] +pub struct UpdateTaskStatusTool { + pub id: String, + pub status: String, +} +#[macros::mcp_tool(name = "list_active_tasks", description = "List active tasks")] +#[derive(Debug, Deserialize, Serialize, macros::JsonSchema)] +pub struct ListActiveTasksTool {} +#[macros::mcp_tool(name = "store_snippet", description = "Store code snippet")] +#[derive(Debug, Deserialize, Serialize, macros::JsonSchema)] +pub struct StoreSnippetTool { + pub name: String, + pub language: String, + pub code: String, + pub description: String, +} +#[macros::mcp_tool(name = "search_snippets", description = "Search snippets")] +#[derive(Debug, Deserialize, Serialize, macros::JsonSchema)] +pub struct SearchSnippetsTool { + pub query: String, +} +#[macros::mcp_tool(name = "delete_snippet", description = "Delete snippet")] +#[derive(Debug, Deserialize, Serialize, macros::JsonSchema)] +pub struct DeleteSnippetTool { + pub name: String, +} +#[macros::mcp_tool( + name = "log_decision", + description = "Log architectural decision record" +)] +#[derive(Debug, Deserialize, Serialize, macros::JsonSchema)] +pub struct LogDecisionTool { + pub title: String, + pub context: String, + pub decision: String, + pub consequence: String, +} +#[macros::mcp_tool(name = "query_decisions", description = "Query decision records")] +#[derive(Debug, Deserialize, Serialize, macros::JsonSchema)] +pub struct QueryDecisionsTool { + pub query: Option, +} +#[macros::mcp_tool( + name = "merge_entities", + description = "Merge source entity into target entity" +)] +#[derive(Debug, Deserialize, Serialize, macros::JsonSchema)] +pub struct MergeEntitiesTool { + #[serde(rename = "sourceEntity")] + pub source_entity: String, + #[serde(rename = "targetEntity")] + pub target_entity: String, +} +#[macros::mcp_tool(name = "find_orphans", description = "Find entities with no relations")] +#[derive(Debug, Deserialize, Serialize, macros::JsonSchema)] +pub struct FindOrphansTool {} +#[macros::mcp_tool( + name = "learn_preference", + description = "Store an AI behavioral preference" +)] +#[derive(Debug, Deserialize, Serialize, macros::JsonSchema)] +pub struct LearnPreferenceTool { + pub key: String, + pub value: String, +} +#[macros::mcp_tool( + name = "read_preferences", + description = "Read all learned behavioral preferences" +)] +#[derive(Debug, Deserialize, Serialize, macros::JsonSchema)] +pub struct ReadPreferencesTool {} +#[macros::mcp_tool(name = "log_error_fix", description = "Log an error fix")] +#[derive(Debug, Deserialize, Serialize, macros::JsonSchema)] +pub struct LogErrorFixTool { + pub signature: String, + pub solution: String, + pub git_commit: Option, + pub git_branch: Option, +} +#[macros::mcp_tool(name = "search_error_fixes", description = "Search error fixes")] +#[derive(Debug, Deserialize, Serialize, macros::JsonSchema)] +pub struct SearchErrorFixesTool { + pub query: String, +} +#[macros::mcp_tool(name = "pin_file", description = "Pin a file to working set")] +#[derive(Debug, Deserialize, Serialize, macros::JsonSchema)] +pub struct PinFileTool { + pub namespace: String, + pub file_path: String, +} +#[macros::mcp_tool(name = "unpin_file", description = "Unpin a file")] +#[derive(Debug, Deserialize, Serialize, macros::JsonSchema)] +pub struct UnpinFileTool { + pub namespace: String, + pub file_path: String, +} +#[macros::mcp_tool(name = "list_pinned_files", description = "List pinned files")] +#[derive(Debug, Deserialize, Serialize, macros::JsonSchema)] +pub struct ListPinnedFilesTool { + pub namespace: Option, +} +#[macros::mcp_tool(name = "add_session_summary", description = "Add a session summary")] +#[derive(Debug, Deserialize, Serialize, macros::JsonSchema)] +pub struct AddSessionSummaryTool { + pub summary: String, + pub namespace: String, +} +#[macros::mcp_tool(name = "get_project_timeline", description = "Get project timeline")] +#[derive(Debug, Deserialize, Serialize, macros::JsonSchema)] +pub struct GetProjectTimelineTool { + pub namespace: Option, +} +#[macros::mcp_tool( + name = "leave_handoff_memo", + description = "Leave memo for next session" +)] +#[derive(Debug, Deserialize, Serialize, macros::JsonSchema)] +pub struct LeaveHandoffMemoTool { + pub content: String, + pub namespace: String, +} +#[macros::mcp_tool(name = "read_handoff_memos", description = "Read pending memos")] +#[derive(Debug, Deserialize, Serialize, macros::JsonSchema)] +pub struct ReadHandoffMemosTool { + pub namespace: Option, +} +#[macros::mcp_tool(name = "clear_handoff_memos", description = "Clear read memos")] +#[derive(Debug, Deserialize, Serialize, macros::JsonSchema)] +pub struct ClearHandoffMemosTool { + pub ids: Vec, +} +#[macros::mcp_tool(name = "update_env_fingerprint", description = "Update env versions")] +#[derive(Debug, Deserialize, Serialize, macros::JsonSchema)] +pub struct UpdateEnvFingerprintTool { + pub namespace: String, + pub tool_versions: std::collections::HashMap, +} +#[macros::mcp_tool(name = "read_env_fingerprint", description = "Read env footprint")] +#[derive(Debug, Deserialize, Serialize, macros::JsonSchema)] +pub struct ReadEnvFingerprintTool { + pub namespace: String, +} +#[macros::mcp_tool(name = "log_env_requirement", description = "Log env var requirement")] +#[derive(Debug, Deserialize, Serialize, macros::JsonSchema)] +pub struct LogEnvRequirementTool { + pub namespace: String, + pub key: String, + pub description: String, + pub is_secret: bool, +} +#[macros::mcp_tool(name = "add_milestone", description = "Add project milestone")] +#[derive(Debug, Deserialize, Serialize, macros::JsonSchema)] +pub struct AddMilestoneTool { + pub title: String, + pub namespace: String, +} +#[macros::mcp_tool(name = "update_milestone", description = "Update milestone status")] +#[derive(Debug, Deserialize, Serialize, macros::JsonSchema)] +pub struct UpdateMilestoneTool { + pub id: String, + pub status: String, +} +#[macros::mcp_tool(name = "list_milestones", description = "List milestones")] +#[derive(Debug, Deserialize, Serialize, macros::JsonSchema)] +pub struct ListMilestonesTool { + pub namespace: Option, +} +#[macros::mcp_tool( + name = "generate_standup_report", + description = "Generate a standup report" +)] +#[derive(Debug, Deserialize, Serialize, macros::JsonSchema)] +pub struct GenerateStandupReportTool { + pub namespace: String, + pub hours_lookback: u64, +} +#[macros::mcp_tool(name = "register_environment", description = "Register an environment")] +#[derive(Debug, Deserialize, Serialize, macros::JsonSchema)] +pub struct RegisterEnvironmentTool { + pub namespace: String, + pub name: String, + pub url: String, + pub description: String, + pub requires_vpn: bool, +} +#[macros::mcp_tool( + name = "get_environment_details", + description = "Get environment details" +)] +#[derive(Debug, Deserialize, Serialize, macros::JsonSchema)] +pub struct GetEnvironmentDetailsTool { + pub namespace: String, +} +#[macros::mcp_tool( + name = "add_pr_checklist_item", + description = "Add a PR checklist item" +)] +#[derive(Debug, Deserialize, Serialize, macros::JsonSchema)] +pub struct AddPrChecklistItemTool { + pub namespace: String, + pub description: String, +} +#[macros::mcp_tool(name = "get_pr_checklist", description = "Get the PR checklist")] +#[derive(Debug, Deserialize, Serialize, macros::JsonSchema)] +pub struct GetPrChecklistTool { + pub namespace: String, +} +#[macros::mcp_tool(name = "clear_pr_checklist", description = "Clear the PR checklist")] +#[derive(Debug, Deserialize, Serialize, macros::JsonSchema)] +pub struct ClearPrChecklistTool { + pub namespace: String, +} +#[macros::mcp_tool(name = "log_tech_debt", description = "Log tech debt")] +#[derive(Debug, Deserialize, Serialize, macros::JsonSchema)] +pub struct LogTechDebtTool { + pub namespace: String, + pub description: String, + pub ideal_solution: String, + pub git_commit: Option, + pub git_branch: Option, +} +#[macros::mcp_tool(name = "resolve_tech_debt", description = "Resolve tech debt")] +#[derive(Debug, Deserialize, Serialize, macros::JsonSchema)] +pub struct ResolveTechDebtTool { + pub id: String, +} +#[macros::mcp_tool(name = "list_tech_debt", description = "List tech debt")] +#[derive(Debug, Deserialize, Serialize, macros::JsonSchema)] +pub struct ListTechDebtTool { + pub namespace: String, + pub include_resolved: bool, +} +#[macros::mcp_tool(name = "save_context_workspace", description = "Save current context")] +#[derive(Debug, Deserialize, Serialize, macros::JsonSchema)] +pub struct SaveContextWorkspaceTool { + pub namespace: String, + pub name: String, + pub pinned_files: Vec, + pub active_task_ids: Vec, +} +#[macros::mcp_tool(name = "load_context_workspace", description = "Load context")] +#[derive(Debug, Deserialize, Serialize, macros::JsonSchema)] +pub struct LoadContextWorkspaceTool { + pub namespace: String, + pub name: String, +} +#[macros::mcp_tool(name = "list_context_workspaces", description = "List contexts")] +#[derive(Debug, Deserialize, Serialize, macros::JsonSchema)] +pub struct ListContextWorkspacesTool { + pub namespace: String, +} +#[macros::mcp_tool(name = "omni_search", description = "Search across all memory vaults")] +#[derive(Debug, Deserialize, Serialize, macros::JsonSchema)] +pub struct OmniSearchTool { + pub query: String, + pub namespace: Option, +} +#[macros::mcp_tool( + name = "get_project_health", + description = "Get project health metrics" +)] +#[derive(Debug, Deserialize, Serialize, macros::JsonSchema)] +pub struct GetProjectHealthTool { + pub namespace: String, +}