Compare commits
2
Commits
ccc268d807
...
d4635ab6dc
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d4635ab6dc | ||
|
|
0e9748f797 |
No files matched your search
Generated
+264
-1
@@ -126,6 +126,26 @@ version = "1.4.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c3d036a3c4ab069c7b410a2ce876bd74808d2d0888a82667669f8e783a898bf1"
|
||||
|
||||
[[package]]
|
||||
name = "arboard"
|
||||
version = "3.6.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0348a1c054491f4bfe6ab86a7b6ab1e44e45d899005de92f58b3df180b36ddaf"
|
||||
dependencies = [
|
||||
"clipboard-win",
|
||||
"image",
|
||||
"log",
|
||||
"objc2",
|
||||
"objc2-app-kit",
|
||||
"objc2-core-foundation",
|
||||
"objc2-core-graphics",
|
||||
"objc2-foundation",
|
||||
"parking_lot",
|
||||
"percent-encoding",
|
||||
"windows-sys 0.59.0",
|
||||
"x11rb",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "arc-swap"
|
||||
version = "1.9.2"
|
||||
@@ -523,6 +543,15 @@ version = "1.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1c133bc6a41be0d194c306b5506d15e6feeea7b1d6604bd3f8310dfb2ca96486"
|
||||
|
||||
[[package]]
|
||||
name = "clipboard-win"
|
||||
version = "5.4.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bde03770d3df201d4fb868f2c9c59e66a3e4e2bd06692a0fe701e7103c7e84d4"
|
||||
dependencies = [
|
||||
"error-code",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "color_quant"
|
||||
version = "1.1.0"
|
||||
@@ -935,6 +964,16 @@ dependencies = [
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "dispatch2"
|
||||
version = "0.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1e0e367e4e7da84520dedcac1901e4da967309406d1e51017ae1abfb97adbd38"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"objc2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "displaydoc"
|
||||
version = "0.2.7"
|
||||
@@ -1040,6 +1079,12 @@ dependencies = [
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "error-code"
|
||||
version = "3.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0b5343afd4a8365a643ac588dab4cf234a190c7f6c88c9f6dd6ffe00837661b7"
|
||||
|
||||
[[package]]
|
||||
name = "esaxx-rs"
|
||||
version = "0.1.10"
|
||||
@@ -1288,6 +1333,16 @@ dependencies = [
|
||||
"version_check",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "gethostname"
|
||||
version = "1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1bd49230192a3797a9a4d6abe9b3eed6f7fa4c8a8a4947977c6f80025f92cbd8"
|
||||
dependencies = [
|
||||
"rustix",
|
||||
"windows-link",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "getrandom"
|
||||
version = "0.2.17"
|
||||
@@ -1333,6 +1388,21 @@ dependencies = [
|
||||
"weezl",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "git2"
|
||||
version = "0.19.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b903b73e45dc0c6c596f2d37eccece7c1c8bb6e4407b001096387c63d0d93724"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"libc",
|
||||
"libgit2-sys",
|
||||
"log",
|
||||
"openssl-probe 0.1.6",
|
||||
"openssl-sys",
|
||||
"url",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "h2"
|
||||
version = "0.4.19"
|
||||
@@ -1860,6 +1930,20 @@ dependencies = [
|
||||
"cc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "libgit2-sys"
|
||||
version = "0.17.0+1.8.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "10472326a8a6477c3c20a64547b0059e4b0d086869eee31e6d7da728a8eb7224"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"libc",
|
||||
"libssh2-sys",
|
||||
"libz-sys",
|
||||
"openssl-sys",
|
||||
"pkg-config",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "libm"
|
||||
version = "0.2.16"
|
||||
@@ -1875,6 +1959,32 @@ dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "libssh2-sys"
|
||||
version = "0.3.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0f5eb74291e8691cab524a01274a1b1e7742b1a94f29d8b101d8aadc8372c1cd"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"libc",
|
||||
"libz-sys",
|
||||
"openssl-sys",
|
||||
"pkg-config",
|
||||
"vcpkg",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "libz-sys"
|
||||
version = "1.1.29"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "85bc9657773828b90eeb625adff10eeac83cc21bbfd8e23a03eaa8a33c9e28d9"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"libc",
|
||||
"pkg-config",
|
||||
"vcpkg",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "linux-raw-sys"
|
||||
version = "0.12.1"
|
||||
@@ -1993,13 +2103,18 @@ dependencies = [
|
||||
name = "mcp-memory-server"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"arboard",
|
||||
"async-trait",
|
||||
"axum",
|
||||
"base64 0.23.1",
|
||||
"chrono",
|
||||
"clap",
|
||||
"clipboard-win",
|
||||
"dirs 7.0.0",
|
||||
"fastembed",
|
||||
"futures-util",
|
||||
"git2",
|
||||
"image",
|
||||
"redb",
|
||||
"reqwest 0.13.5",
|
||||
"rmcp",
|
||||
@@ -2014,6 +2129,10 @@ dependencies = [
|
||||
"tracing",
|
||||
"tracing-appender",
|
||||
"tracing-subscriber",
|
||||
"tree-sitter",
|
||||
"tree-sitter-python",
|
||||
"tree-sitter-rust",
|
||||
"tree-sitter-typescript",
|
||||
"uuid",
|
||||
"winres",
|
||||
]
|
||||
@@ -2173,7 +2292,7 @@ dependencies = [
|
||||
"libc",
|
||||
"log",
|
||||
"openssl",
|
||||
"openssl-probe",
|
||||
"openssl-probe 0.2.1",
|
||||
"openssl-sys",
|
||||
"schannel",
|
||||
"security-framework",
|
||||
@@ -2329,6 +2448,79 @@ dependencies = [
|
||||
"tracing-subscriber",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "objc2"
|
||||
version = "0.6.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3a12a8ed07aefc768292f076dc3ac8c48f3781c8f2d5851dd3d98950e8c5a89f"
|
||||
dependencies = [
|
||||
"objc2-encode",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "objc2-app-kit"
|
||||
version = "0.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d49e936b501e5c5bf01fda3a9452ff86dc3ea98ad5f283e1455153142d97518c"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"objc2",
|
||||
"objc2-core-graphics",
|
||||
"objc2-foundation",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "objc2-core-foundation"
|
||||
version = "0.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"dispatch2",
|
||||
"objc2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "objc2-core-graphics"
|
||||
version = "0.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e022c9d066895efa1345f8e33e584b9f958da2fd4cd116792e15e07e4720a807"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"dispatch2",
|
||||
"objc2",
|
||||
"objc2-core-foundation",
|
||||
"objc2-io-surface",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "objc2-encode"
|
||||
version = "4.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ef25abbcd74fb2609453eb695bd2f860d389e457f67dc17cafc8b8cbc89d0c33"
|
||||
|
||||
[[package]]
|
||||
name = "objc2-foundation"
|
||||
version = "0.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e3e0adef53c21f888deb4fa59fc59f7eb17404926ee8a6f59f5df0fd7f9f3272"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"objc2",
|
||||
"objc2-core-foundation",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "objc2-io-surface"
|
||||
version = "0.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "180788110936d59bab6bd83b6060ffdfffb3b922ba1396b312ae795e1de9d81d"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"objc2",
|
||||
"objc2-core-foundation",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "once_cell"
|
||||
version = "1.21.4"
|
||||
@@ -2394,6 +2586,12 @@ dependencies = [
|
||||
"syn 2.0.119",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "openssl-probe"
|
||||
version = "0.1.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e"
|
||||
|
||||
[[package]]
|
||||
name = "openssl-probe"
|
||||
version = "0.2.1"
|
||||
@@ -3999,6 +4197,54 @@ dependencies = [
|
||||
"tracing-log",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tree-sitter"
|
||||
version = "0.23.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0203df02a3b6dd63575cc1d6e609edc2181c9a11867a271b25cfd2abff3ec5ca"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"regex",
|
||||
"regex-syntax",
|
||||
"tree-sitter-language",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tree-sitter-language"
|
||||
version = "0.1.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ca0d1bf6fdd806e43ae5198f82f527056d359def39e54e67a0f478ac09dac081"
|
||||
|
||||
[[package]]
|
||||
name = "tree-sitter-python"
|
||||
version = "0.23.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3d065aaa27f3aaceaf60c1f0e0ac09e1cb9eb8ed28e7bcdaa52129cffc7f4b04"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"tree-sitter-language",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tree-sitter-rust"
|
||||
version = "0.23.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ca8ccb3e3a3495c8a943f6c3fd24c3804c471fd7f4f16087623c7fa4c0068e8a"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"tree-sitter-language",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tree-sitter-typescript"
|
||||
version = "0.23.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6c5f76ed8d947a75cc446d5fccd8b602ebf0cde64ccf2ffa434d873d7a575eff"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"tree-sitter-language",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "try-lock"
|
||||
version = "0.2.5"
|
||||
@@ -4584,6 +4830,23 @@ version = "0.6.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3ad82d2a33cdc9674dc7465672f271e096168fcdbe0f799d9e6db8c5892679dc"
|
||||
|
||||
[[package]]
|
||||
name = "x11rb"
|
||||
version = "0.13.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9993aa5be5a26815fe2c3eacfc1fde061fc1a1f094bf1ad2a18bf9c495dd7414"
|
||||
dependencies = [
|
||||
"gethostname",
|
||||
"rustix",
|
||||
"x11rb-protocol",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "x11rb-protocol"
|
||||
version = "0.13.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ea6fc2961e4ef194dcbfe56bb845534d0dc8098940c7e5c012a258bfec6701bd"
|
||||
|
||||
[[package]]
|
||||
name = "y4m"
|
||||
version = "0.8.0"
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
# Effective Discourse: LLM Prompting Guide for MCP Memory
|
||||
|
||||
To get the most out of the Antigravity MCP Memory server and its advanced developer tools, use specific phrases that clearly state your intent. This guides the LLM to use the most efficient tools, reducing token consumption, speeding up time-to-resolve (T2R), and avoiding brute-force file reading.
|
||||
|
||||
## 1. Codebase Exploration & Token Efficiency
|
||||
When entering a new file, avoid having the LLM read the entire contents blindly.
|
||||
* **Don't say:** "Read server.rs and tell me what it does." *(Consumes massive tokens)*
|
||||
* **Do say:** "Extract the AST skeleton of server.rs to understand its structure first."
|
||||
* **Tool Triggered:** ␍ead_file_skeleton
|
||||
|
||||
## 2. Debugging & Log Parsing
|
||||
Stop copy-pasting giant walls of logs into the chat interface.
|
||||
* **Don't say:** "Here is the error: [paste 500 lines of logs]"
|
||||
* **Do say:** "The daemon crashed. Fetch the recent logs from daemon.log." or "Watch the process logs for server.log."
|
||||
* **Tool Triggered:** get_recent_logs, watch_process_logs
|
||||
|
||||
## 3. Git & Context Handoff
|
||||
When you've been working independently and need to loop the LLM back in on your current state.
|
||||
* **Don't say:** "I changed some files, here are the diffs..."
|
||||
* **Do say:** "Get the active git worktree context to review my uncommitted changes before we continue."
|
||||
* **Tool Triggered:** get_active_worktree_context
|
||||
|
||||
## 4. Clipboard Watch Mode (Research & Triage)
|
||||
When you are doing intense debugging across StackOverflow, logs, and docs, use the clipboard watcher to auto-ingest your breadcrumbs.
|
||||
* **Action:** Ask the LLM to turn it on: "Enable clipboard watch mode."
|
||||
* **Do say:** "I'm going to reproduce the bug and copy some stack traces and IDs. Give me a minute, then read my latest sticky notes to catch up."
|
||||
* **Tool Triggered:** oggle_clipboard_watch_mode, followed by internal Sticky Note reads.
|
||||
|
||||
## 5. Neovim Ghost Text (Live Previews)
|
||||
Keep your workflow entirely within your editor rather than copy-pasting code blocks from the chat.
|
||||
* **Don't say:** "Write the updated function here so I can copy-paste it."
|
||||
* **Do say:** "Push this refactor to my active Neovim buffer as ghost text so I can review it in-line."
|
||||
* **Tool Triggered:**
|
||||
vim_set_preview
|
||||
|
||||
## 6. Graph & Memory Management
|
||||
Actively instruct the LLM to maintain its own memory constraints and organize data.
|
||||
* **Do say:** "Log this architectural decision in the knowledge graph."
|
||||
* **Do say:** "Add a sticky note with the test database credentials for this session."
|
||||
* **Do say:** "Create a milestone for the 'Rich Clipboard' feature and break it down into active tasks."
|
||||
* **Tools Triggered:** create_entities, log_decision, dd_sticky_note, dd_milestone, dd_task
|
||||
|
||||
By phrasing requests around *actions* rather than *information retrieval*, the LLM is primed to leverage the rich MCP toolset built into the Antigravity Memory Server.
|
||||
@@ -1,125 +1,161 @@
|
||||
# mcp-memory
|
||||
A high-performance, persistent Knowledge Graph and Context daemon for Antigravity, implementing the Model Context Protocol (MCP).
|
||||
`
|
||||
## Overview
|
||||
mcp-memory acts as the persistent "brain" for the agy CLI agents. It tracks entities, relations, background tasks, engineering debt, and architectural decisions across sessions.
|
||||
`
|
||||
To eliminate heavy Cross-OS I/O penalties when using WSL and Windows simultaneously, mcp-memory operates using a **Dual-Transport Leader/Stub Architecture**:
|
||||
* **The Server (mcp-memory-server)**: Runs natively on the Windows host. It binds to .0.0.0:3000, serving standard stdio to the primary Windows agy instance while simultaneously hosting an Axum HTTP server for secondary clients.
|
||||
* **The Stub (mcp-memory-stub)**: An ultra-lightweight proxy binary. WSL agy instances run this native Linux stub, which transparently pipes stdio JSON-RPC traffic over the network to the Windows HTTP server (http://127.0.0.1:3000), completely bypassing WSL NTFS mounts. It features full MPSC queue buffering and a WebSocket reconnect handshake (notifications/tools/list_changed) so that tools automatically refresh seamlessly without disconnecting the CLI if the background server restarts.
|
||||
`
|
||||
# mcp-memory
|
||||
A high-performance, persistent Knowledge Graph and Context daemon for Antigravity, implementing the Model Context Protocol (MCP).
|
||||
`
|
||||
## Overview
|
||||
mcp-memory acts as the persistent "brain" for the agy CLI agents. It tracks entities, relations, background tasks, engineering debt, and architectural decisions across sessions.
|
||||
`
|
||||
To eliminate heavy Cross-OS I/O penalties when using WSL and Windows simultaneously, mcp-memory operates using a **Dual-Transport Leader/Stub Architecture**:
|
||||
* **The Server (mcp-memory-server)**: Runs natively on the Windows host. It binds to .0.0.0:3000, serving standard stdio to the primary Windows agy instance while simultaneously hosting an Axum HTTP server for secondary clients.
|
||||
* **The Stub (mcp-memory-stub)**: An ultra-lightweight proxy binary. WSL agy instances run this native Linux stub, which transparently pipes stdio JSON-RPC traffic over the network to the Windows HTTP server (http://127.0.0.1:3000), completely bypassing WSL NTFS mounts. It features full MPSC queue buffering and a WebSocket reconnect handshake (notifications/tools/list_changed) so that tools automatically refresh seamlessly without disconnecting the CLI if the background server restarts.
|
||||
`
|
||||
## Quick Start & Usage
|
||||
`
|
||||
### 1. Windows Installation (The Server & Stub)
|
||||
Compile the main daemon and lightweight stub natively for Windows:
|
||||
```powershell
|
||||
cargo build --release
|
||||
Copy-Item target\release\mcp-memory-server.exe C:\Users\reazul.ashraf\.local\bin\mcp-memory-server.exe
|
||||
Copy-Item target\release\mcp-memory-stub.exe C:\Users\reazul.ashraf\.local\bin\mcp-memory-stub.exe
|
||||
```
|
||||
`
|
||||
**Step 1:** To bypass Antigravity's lazy-loading and ensure the server is instantly available for WSL, configure your PowerShell profile to auto-start the background server when you open a terminal:
|
||||
```powershell
|
||||
# Add this to your PowerShell profile:
|
||||
if (-not (Get-Process mcp-memory-server -ErrorAction SilentlyContinue)) { Start-Process -FilePath "C:\Users\reazul.ashraf\.local\bin\mcp-memory-server.exe" -WindowStyle Hidden -ErrorAction SilentlyContinue }
|
||||
```
|
||||
|
||||
**Shutting Down & Managing:** If you need to stop, start, or restart the background daemon (e.g., to replace the executable after a recompile), you MUST NEVER use brute-force OS kill commands (e.g., Stop-Process, pkill, or kill). Instead, you MUST ALWAYS use the built-in graceful shutdown mechanisms, which are wrapped neatly in the `justfile`:
|
||||
```powershell
|
||||
### 1. Windows Installation (The Server & Stub)
|
||||
|
||||
To enforce strict process safety and eliminate file locks on Windows, the build, deploy, and execution lifecycle are entirely decoupled in the justfile.
|
||||
|
||||
**The Golden Rule:** You must gracefully stop the server before deploying a new binary. Deploy recipes only copy files; they do not kill processes.
|
||||
|
||||
The easiest way to manage this end-to-end (Stop -> Build -> Deploy -> Start) is using the chaining commands:
|
||||
``powershell
|
||||
# For the main server:
|
||||
just all-server-win
|
||||
|
||||
# For the lightweight stubs/nvim servers:
|
||||
just all-stub-win
|
||||
just all-nvim-win
|
||||
``
|
||||
|
||||
If you want to perform these steps manually, you must follow this exact order to avoid NTFS locks:
|
||||
``powershell
|
||||
just stop # 1. Gracefully shut down the background server (TCP 3000)
|
||||
just build-win # 2. Compile the binaries
|
||||
just deploy-win # 3. Move the executables into ~/.local/bin/
|
||||
just start # 4. Spawns the daemon completely detached in the background
|
||||
just verify # 5. Hits the /ping endpoint to ensure liveness
|
||||
``
|
||||
|
||||
**Step 1:** To bypass Antigravity's lazy-loading and ensure the server is instantly available for WSL, configure your PowerShell profile to auto-start the background server when you open a terminal:
|
||||
``powershell
|
||||
# Add this to your PowerShell profile (ensuring it only fires on initial load, not background threads):
|
||||
if ($host.Name -eq 'ConsoleHost' -and -not (Get-Process mcp-memory-server -ErrorAction SilentlyContinue)) {
|
||||
Start-Process -FilePath "C:\Users\reazul.ashraf\.local\bin\mcp-memory-server.exe" -WindowStyle Hidden -ErrorAction SilentlyContinue
|
||||
}
|
||||
``
|
||||
|
||||
**Shutting Down & Managing:** If you need to stop, start, or restart the background daemon, NEVER use brute-force OS kill commands (e.g., Stop-Process, pkill). ALWAYS use the justfile wrappers, which trigger a graceful /shutdown over HTTP:
|
||||
``powershell
|
||||
just start
|
||||
just stop
|
||||
just restart
|
||||
```
|
||||
Alternatively, you can gracefully shut down the server by invoking the executable with the `--exit` flag (`mcp-memory-server.exe --exit`) or hitting the HTTP endpoint (`POST http://127.0.0.1:3000/shutdown`).
|
||||
`
|
||||
**Step 2:** Update your Windows ~/.gemini/config/mcp_config.json to point the CLI to the ultra-lightweight stub (since the server is already running in the background):
|
||||
`json
|
||||
{
|
||||
"mcpServers": {
|
||||
"memory": {
|
||||
"command": "C:\\Users\\reazul.ashraf\\.local\\bin\\mcp-memory-stub.exe",
|
||||
"args": []
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
`
|
||||
### 2. WSL / Linux Installation (The Stub)
|
||||
Compile the ultra-lightweight stub as a native Linux binary directly from WSL (we do not use zigbuild):
|
||||
```powershell
|
||||
just deploy-stub-wsl
|
||||
```
|
||||
`
|
||||
Update your WSL ~/.gemini/config/mcp_config.json:
|
||||
`json
|
||||
{
|
||||
"mcpServers": {
|
||||
"memory": {
|
||||
"command": "/home/riz/.local/bin/mcp-memory-stub",
|
||||
"args": [
|
||||
"--target", "http://127.0.0.1:3000",
|
||||
"--wake-cmd", "powershell.exe -NoProfile -WindowStyle Hidden -Command \"Start-Process -FilePath 'C:\\Users\\reazul.ashraf\\.local\\bin\\mcp-memory-server.exe' -WindowStyle Hidden\""
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
*Note: The --wake-cmd ensures that if you start WSL while Windows is completely asleep, the Linux stub will use WSL interop to silently spin up the Windows daemon in the background before connecting.*
|
||||
`
|
||||
## Push Safety Gates
|
||||
The daemon also operates as a global safety gate for Git. Before pushing code, run:
|
||||
`ash
|
||||
mcp-memory gate verify
|
||||
`
|
||||
This queries the daemon (via HTTP) to confirm if pre-push validation (like running tests via PrePushAuditor) has been cleared by the agent.
|
||||
`
|
||||
``
|
||||
Alternatively, you can gracefully shut down the server by invoking the executable with the --exit flag (mcp-memory-server.exe --exit) or hitting the HTTP endpoint (POST http://127.0.0.1:3000/shutdown).
|
||||
`
|
||||
**Step 2:** Update your Windows ~/.gemini/config/mcp_config.json to point the CLI to the ultra-lightweight stub (since the server is already running in the background):
|
||||
`json
|
||||
{
|
||||
"mcpServers": {
|
||||
"memory": {
|
||||
"command": "C:\\Users\\reazul.ashraf\\.local\\bin\\mcp-memory-stub.exe",
|
||||
"args": []
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
`
|
||||
### 2. WSL / Linux Installation (The Stub)
|
||||
Compile the ultra-lightweight stub as a native Linux binary directly from WSL (we do not use zigbuild):
|
||||
```powershell
|
||||
just deploy-stub-wsl
|
||||
```
|
||||
`
|
||||
Update your WSL ~/.gemini/config/mcp_config.json:
|
||||
`json
|
||||
{
|
||||
"mcpServers": {
|
||||
"memory": {
|
||||
"command": "/home/riz/.local/bin/mcp-memory-stub",
|
||||
"args": [
|
||||
"--target", "http://127.0.0.1:3000",
|
||||
"--wake-cmd", "powershell.exe -NoProfile -WindowStyle Hidden -Command \"Start-Process -FilePath 'C:\\Users\\reazul.ashraf\\.local\\bin\\mcp-memory-server.exe' -WindowStyle Hidden\""
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
*Note: The --wake-cmd ensures that if you start WSL while Windows is completely asleep, the Linux stub will use WSL interop to silently spin up the Windows daemon in the background before connecting.*
|
||||
`
|
||||
## Push Safety Gates
|
||||
The daemon also operates as a global safety gate for Git. Before pushing code, run:
|
||||
`ash
|
||||
mcp-memory gate verify
|
||||
`
|
||||
This queries the daemon (via HTTP) to confirm if pre-push validation (like running tests via PrePushAuditor) has been cleared by the agent.
|
||||
`
|
||||
## Brain Monitor Dashboard
|
||||
The server hosts a live, real-time SPA dashboard called the **Brain Monitor**.
|
||||
|
||||
To view the dashboard, simply navigate to the root endpoint in your browser while the server is running:
|
||||
`http://127.0.0.1:3000/`
|
||||
http://127.0.0.1:3000/
|
||||
|
||||
### Dashboard Features:
|
||||
* **Interactive Knowledge Graph:** A full physics-simulated network graph with node-type coloring, a drag-to-pan canvas, and an interactive Inspector Panel. Click any node to instantly view its stored observations.
|
||||
* **Actionable Kanban Board:** Visually track all active agent Tasks. You can click 'Complete' directly from the UI to trigger a `POST /api/tasks/{id}/complete` REST call back to the daemon without needing the CLI.
|
||||
* **Actionable Kanban Board:** Visually track all active agent Tasks. You can click 'Complete' directly from the UI to trigger a POST /api/tasks/{id}/complete REST call back to the daemon without needing the CLI.
|
||||
* **📋 Inspect Clipboard (Image Capture):** Visually review OS-level clipboard images directly in the browser via the new /api/clipboard/capture endpoint, allowing the agent to dynamically 'see' screenshot errors or UI wireframes.
|
||||
* **Sticky Notes & Search:** Browse your ephemeral notes and utilize the integrated fuzzy-search bar to locate graph entities instantly.
|
||||
* **Native Dark Mode:** Fully styled for modern development environments.
|
||||
|
||||
You can also programmatically query the live backing APIs:
|
||||
`curl http://127.0.0.1:3000/api/stats`
|
||||
`curl http://127.0.0.1:3000/api/graph`
|
||||
`curl http://127.0.0.1:3000/api/tasks`
|
||||
|
||||
## Further Reading
|
||||
For a deep dive into the architecture, the **Redb LSM-tree** embedded database, `tantivy` indexing, and the HTTP SSE event loop, consult the design.md file in this repository.
|
||||
|
||||
## Neovim Integration
|
||||
The linux-nvim and win-nvim MCP servers provide direct Msgpack-RPC communication with Neovim.
|
||||
For this to work flawlessly across multiple Neovim instances (even split across Windows and WSL), you must load the provided gemini-integration.lua file in your Neovim init.lua:
|
||||
`lua
|
||||
dofile("C:/Users/reazul.ashraf/workspace/rust/mcp-memory/gemini-integration.lua")
|
||||
`
|
||||
|
||||
### The "Last Focused Wins" Architecture
|
||||
When you use the gemini-integration.lua script, Neovim acts as an active telemetry broadcaster.
|
||||
Whenever you alt-tab into a Neovim window (FocusGained) or switch files (BufEnter):
|
||||
1. **Fallback Sync:** Neovim instantly writes its unique Session ID (Named Pipe / Unix Socket) to ~/.gemini/active_nvim.txt.
|
||||
2. **WebSocket Telemetry:** Neovim pushes a JSON payload containing the active filename, cursor row, and column via a connectionless UDP datagram to the Rust server's port 3002 listener.
|
||||
3. **UI Broadcast:** The Rust server updates the global state and broadcasts this over WebSockets (/ws) so that the Brain Monitor Dashboard can animate your active file live in the UI!
|
||||
|
||||
### Interactive UDP UI (New)
|
||||
The UI script (`gemini-ui.lua`) provides a deeply integrated, non-blocking pair-programming experience using zero-latency UDP:
|
||||
* **Interactive Prompts:** The agent can trigger native `vim.ui.select` or `vim.ui.input` dialogs in your editor. Your responses are instantly routed back to the agent via UDP.
|
||||
* **Smart Context (`<leader>ai`):** Highlighting code and pressing `<leader>ai` will package your prompt, file, and exact cursor/selection coordinates into a UDP packet and send it directly to the agent without spawning any subprocesses.
|
||||
* **Ghost Text Diffs (Non-Destructive Review):** Instead of modifying your buffers directly, the agent uses `extmarks` to overlay proposed code changes as grayed-out "Ghost Text".
|
||||
* Press `<leader>aa` (**A**gent **A**ccept) to apply the change and notify the agent.
|
||||
* Press `<leader>ar` (**A**gent **R**eject) to dismiss the change and notify the agent.
|
||||
|
||||
### Neovim MCP Tools
|
||||
The LLM agent interacts with your active Neovim session using a dedicated set of MCP tools. *(Note: /nvim/telemetry is strictly a one-way webhook for Neovim; the LLM uses the tools below to interact).*
|
||||
* **vim_goto_line**: Open files and jump cursors directly from the LLM.
|
||||
* **vim_set_diagnostics**: Push inline code review warnings as virtual text.
|
||||
* **vim_get_active_buffer**: Read live, unsaved buffer contents.
|
||||
* **vim_get_cursor**: Fetch precise line/column coordinates.
|
||||
* **vim_get_visual_selection**: Read highlighted code blocks.
|
||||
## Rich Clipboard Tools
|
||||
The memory server provides powerful, cross-OS native clipboard capabilities allowing the agent to inject and extract rich data. Because of the Leader/Stub architecture, all operations map directly to the Windows Host OS clipboard, regardless of whether the agent is running in Windows or WSL Ubuntu:
|
||||
* **Text & HTML:** Read and write plain text or rich HTML formatting (using clipboard-win).
|
||||
* **File Drops (CF_HDROP):** Read absolute file paths that were copied in Windows Explorer, or place file paths into the clipboard so the user can easily Ctrl+V them into IDEs or Explorer. (Note: Linux paths must be translated to \\wsl.localhost\... UNC paths first).
|
||||
* **Images:** Read screenshots, and write generated images directly into the clipboard (using rboard).
|
||||
|
||||
You can also programmatically query the live backing APIs:
|
||||
`curl http://127.0.0.1:3000/ping`
|
||||
`curl http://127.0.0.1:3000/api/stats`
|
||||
`curl http://127.0.0.1:3000/api/graph`
|
||||
`curl http://127.0.0.1:3000/api/tasks`
|
||||
|
||||
## Further Reading
|
||||
For a deep dive into the architecture, the **Redb LSM-tree** embedded database, `tantivy` indexing, and the HTTP SSE event loop, consult the design.md file in this repository.
|
||||
|
||||
## Neovim Integration
|
||||
The linux-nvim and win-nvim MCP servers provide direct Msgpack-RPC communication with Neovim.
|
||||
For this to work flawlessly across multiple Neovim instances (even split across Windows and WSL), you must load the provided gemini-integration.lua file in your Neovim init.lua:
|
||||
`lua
|
||||
dofile("C:/Users/reazul.ashraf/workspace/rust/mcp-memory/gemini-integration.lua")
|
||||
`
|
||||
|
||||
### The "Last Focused Wins" Architecture
|
||||
When you use the gemini-integration.lua script, Neovim acts as an active telemetry broadcaster.
|
||||
Whenever you alt-tab into a Neovim window (FocusGained) or switch files (BufEnter):
|
||||
1. **Fallback Sync:** Neovim instantly writes its unique Session ID (Named Pipe / Unix Socket) to ~/.gemini/active_nvim.txt.
|
||||
2. **WebSocket Telemetry:** Neovim pushes a JSON payload containing the active filename, cursor row, and column via a connectionless UDP datagram to the Rust server's port 3002 listener.
|
||||
3. **UI Broadcast:** The Rust server updates the global state and broadcasts this over WebSockets (/ws) so that the Brain Monitor Dashboard can animate your active file live in the UI!
|
||||
|
||||
### Interactive UDP UI (New)
|
||||
The UI script (`gemini-ui.lua`) provides a deeply integrated, non-blocking pair-programming experience using zero-latency UDP:
|
||||
* **Interactive Prompts:** The agent can trigger native `vim.ui.select` or `vim.ui.input` dialogs in your editor. Your responses are instantly routed back to the agent via UDP.
|
||||
* **Smart Context (`<leader>ai`):** Highlighting code and pressing `<leader>ai` will package your prompt, file, and exact cursor/selection coordinates into a UDP packet and send it directly to the agent without spawning any subprocesses.
|
||||
* **Ghost Text Diffs (Non-Destructive Review):** Instead of modifying your buffers directly, the agent uses `extmarks` to overlay proposed code changes as grayed-out "Ghost Text".
|
||||
* Press `<leader>aa` (**A**gent **A**ccept) to apply the change and notify the agent.
|
||||
* Press `<leader>ar` (**A**gent **R**eject) to dismiss the change and notify the agent.
|
||||
|
||||
### Neovim MCP Tools
|
||||
The LLM agent interacts with your active Neovim session using a dedicated set of MCP tools. *(Note: /nvim/telemetry is strictly a one-way webhook for Neovim; the LLM uses the tools below to interact).*
|
||||
* **vim_goto_line**: Open files and jump cursors directly from the LLM.
|
||||
* **vim_set_diagnostics**: Push inline code review warnings as virtual text.
|
||||
* **vim_get_active_buffer**: Read live, unsaved buffer contents.
|
||||
* **vim_get_cursor**: Fetch precise line/column coordinates.
|
||||
* **vim_get_visual_selection**: Read highlighted code blocks.
|
||||
|
||||
|
||||
## Enhanced Developer Tools
|
||||
- **AST Skeleton Extractor (␍ead_file_skeleton)**: Uses ree-sitter to parse large code files (Rust, Python, TS/JS) and return an AST structural outline containing only Imports, Structs, Enums, Traits, and Functions, massively saving LLM tokens.
|
||||
- **Git Context (get_active_worktree_context)**: Uses native git2 C-bindings to retrieve the active branch, modified files, and a truncated local patch diff without shell parsing overhead.
|
||||
- **Rolling Log Watcher**: Background background polling endpoints (watch_process_logs / get_recent_logs) to instantly debug daemon crashes.
|
||||
- **Clipboard Watch Mode ( oggle_clipboard_watch_mode)**: Background daemon thread that auto-ingests your Ctrl+C clipboard activity directly into Knowledge Graph StickyNotes while you debug.
|
||||
- **Ghost Text Previews (
|
||||
vim_set_preview)**: Pushes proposed LLM code diffs directly into Neovim buffers as ephemeral virtual text.
|
||||
- [Prompting Guide & Effective Discourse](./PROMPTING_GUIDE.md): Learn how to phrase prompts to get the most out of the agent and memory server.
|
||||
@@ -0,0 +1,4 @@
|
||||
Write-Host "Building Windows Server..." -ForegroundColor Cyan
|
||||
Building Windows Server...
|
||||
cargo build --release -p mcp-memory-server
|
||||
Compiling tantivy v0.26.2
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 82 KiB |
@@ -105,6 +105,9 @@ The mcp-memory daemon employs a strict Server/Client paradigm to maintain separa
|
||||
### The Windows Background Daemon (`mcp-memory-server.exe`)
|
||||
The server executable is responsible exclusively for running the Axum HTTP and WebSockets daemon and managing the Knowledge Graph. It does not contain any proxy logic. If port 3000 is already in use by an existing server instance, it gracefully exits instead of attempting to run.
|
||||
|
||||
* **Non-Blocking Startup & Indexing:** Upon launch, the server uses a `tokio::spawn` task to rebuild the Tantivy index in the background. This ensures the Axum HTTP server binds immediately to port 3000, allowing instantaneous liveness checks via the `/ping` endpoint without waiting for the synchronous I/O operations of the index rebuild to complete.
|
||||
* **Robust Background Detachment:** To ensure the server survives the death of the parent shell that spawns it (such as a deployment script), it uses Windows `WScript.Shell` (via `start_server.ps1`) to spawn the binary with a strictly hidden window style (`SW_HIDE`). This completely detaches it from the parent process tree, preventing `CTRL_CLOSE_EVENT` signals from killing the background daemon when the deploying terminal closes.
|
||||
|
||||
### The Client Proxy (`mcp-memory-stub.exe`)
|
||||
All Antigravity sessions (Windows and WSL) use the lightweight `mcp-memory-stub` as their proxy. The stub connects to the server via WebSockets and acts as the bridge for standard `stdio` JSON-RPC traffic.
|
||||
* **Startup via Interop/Spawn:** Upon launch, the stub attempts to connect to the Windows host on port 3000. If the server is offline, the stub automatically executes a spawn command (e.g., executing `Start-Process` natively, or via WSL interop) to silently wake up the Windows Leader before commencing the proxy loop.
|
||||
@@ -177,3 +180,10 @@ To prevent context window bloat, the Redb engine will enforce TTLs on ephemeral
|
||||
### B. Proactive Subagent Triggers
|
||||
The daemon will eventually gain the ability to autonomously spawn background subagents (like the `BugDiagnostician`) when specific events are detected in the webhook telemetry, rather than relying strictly on the human user to initiate the agent.
|
||||
|
||||
## 11. Advanced Clipboard Capabilities
|
||||
The memory server bypasses typical cross-OS Linux/Windows clipboard restrictions by proxying operations from the WSL stub back to the Windows Axum host.
|
||||
It supports reading and writing rich formats natively to the Windows Host OS using the clipboard-win and rboard crates:
|
||||
* **Plain & Rich Text:** CF_UNICODETEXT and CF_HTML are supported for rich copying/pasting.
|
||||
* **File Drops (CF_HDROP):** The server can parse file lists copied from Windows Explorer, and can inversely synthesize file drops into the clipboard from absolute paths.
|
||||
* **Images (CF_BITMAP):** The server natively rasterizes clipboard bitmaps to JPEG on read, and can write raw RgbaImage buffers back to the clipboard on write.
|
||||
* **Developer Tooling:** read_file_skeleton (AST), get_active_worktree_context (Git), get_recent_logs, toggle_clipboard_watch_mode.
|
||||
@@ -0,0 +1,10 @@
|
||||
$lines = Get-Content justfile
|
||||
$valid_lines = @()
|
||||
|
||||
foreach ($line in $lines) {
|
||||
if ($line -match "Building Windows Nvim" -or $line -match "cargo build --release -p mcp-memory-win-nvim" -or $line -match "^build-nvim-win:") {
|
||||
continue
|
||||
}
|
||||
$valid_lines += $line
|
||||
}
|
||||
$valid_lines | Set-Content justfile
|
||||
@@ -8,15 +8,15 @@ all: build-all deploy-all
|
||||
|
||||
# Build everything across Windows and WSL
|
||||
build-all: build-win build-wsl
|
||||
Write-Host "Build complete across both OS boundaries." -ForegroundColor Green
|
||||
@Write-Host "Build complete across both OS boundaries." -ForegroundColor Green
|
||||
|
||||
# Deploy everything across Windows and WSL
|
||||
deploy-all: deploy-win deploy-wsl
|
||||
Write-Host "Deployment complete across both OS boundaries." -ForegroundColor Green
|
||||
@Write-Host "Deployment complete across both OS boundaries." -ForegroundColor Green
|
||||
|
||||
# Gracefully shut down the running server
|
||||
shutdown-server:
|
||||
Write-Host "Shutting down MCP server gracefully..." -ForegroundColor Cyan
|
||||
@Write-Host "Shutting down MCP server gracefully..." -ForegroundColor Cyan
|
||||
-$token = Get-Content "$env:USERPROFILE\.gemini\mcp_memory\admin.token" -ErrorAction SilentlyContinue; if ($token) { Invoke-RestMethod -Uri "http://127.0.0.1:3000/shutdown" -Method Post -Headers @{ Authorization = "Bearer $token" } -ErrorAction SilentlyContinue } else { Invoke-RestMethod -Uri "http://127.0.0.1:3000/shutdown" -Method Post -ErrorAction SilentlyContinue }
|
||||
Start-Sleep -Seconds 1
|
||||
-if (Get-Process mcp-memory-server, mcp-memory-stub, mcp-memory-win-nvim -ErrorAction SilentlyContinue) { Stop-Process -Name mcp-memory-server, mcp-memory-stub, mcp-memory-win-nvim -Force -ErrorAction SilentlyContinue }
|
||||
@@ -28,61 +28,93 @@ build-win: build-server build-stub-win build-nvim-win
|
||||
# Deploy Windows-native binaries
|
||||
deploy-win: deploy-server deploy-stub-win deploy-nvim-win deploy-rules-win
|
||||
|
||||
# =========================================================
|
||||
# 1. LIFECYCLE (Start / Stop / Restart)
|
||||
# =========================================================
|
||||
|
||||
# Stop the background server
|
||||
stop port="3000":
|
||||
@Write-Host "Shutting down MCP server gracefully on port {{port}}..." -ForegroundColor Cyan
|
||||
-$token = Get-Content "$env:USERPROFILE\.gemini\mcp_memory\admin.token" -ErrorAction SilentlyContinue; if ($token) { Invoke-RestMethod -Uri "http://127.0.0.1:{{port}}/shutdown" -Method Post -Headers @{ Authorization = "Bearer $token" } -ErrorAction SilentlyContinue } else { Invoke-RestMethod -Uri "http://127.0.0.1:{{port}}/shutdown" -Method Post -ErrorAction SilentlyContinue }
|
||||
Start-Sleep -Seconds 1
|
||||
-Get-NetTCPConnection -LocalPort {{port}} -State Listen -ErrorAction SilentlyContinue | Select-Object -ExpandProperty OwningProcess | ForEach-Object { Stop-Process -Id $_ -Force -ErrorAction SilentlyContinue }
|
||||
Start-Sleep -Seconds 2
|
||||
|
||||
# Start the server in the background (nohup equivalent)
|
||||
start port="3000":
|
||||
@if (Get-NetTCPConnection -LocalPort {{port}} -State Listen -ErrorAction SilentlyContinue) { Write-Host '⚠️ Server is already running on port {{port}}. Skipping start.' -ForegroundColor Yellow; exit 0 }; Write-Host 'Starting MCP server in background on port {{port}}...' -ForegroundColor Cyan; $env:MCP_PORT='{{port}}'; $env:MCP_UDP_PORT1=$([int]{{port}} + 1); $env:MCP_UDP_PORT2=$([int]{{port}} + 2); .\start_server.ps1 -Port {{port}}; Write-Host 'Waiting up to 40 seconds for /ping endpoint...' -ForegroundColor Cyan; for ($i = 1; $i -le 40; $i++) { try { $res = Invoke-RestMethod -Uri 'http://127.0.0.1:{{port}}/ping' -ErrorAction Stop; Write-Host '✅ Server is UP!' -ForegroundColor Green; exit 0 } catch { Start-Sleep -Seconds 1 } }; Write-Error '❌ Server failed to start on port {{port}} within 40 seconds.'; exit 1
|
||||
|
||||
# Verify the deployment is healthy and serving correct content
|
||||
verify port="3000":
|
||||
.\verify_deployment.ps1 -Port {{port}}
|
||||
|
||||
# Restart the server and verify
|
||||
restart port="3000": (stop port) (start port) (verify port)
|
||||
|
||||
# =========================================================
|
||||
# 2. BUILD (Compile Binaries)
|
||||
# =========================================================
|
||||
|
||||
build-server:
|
||||
Write-Host "Building Windows Server..." -ForegroundColor Cyan
|
||||
@Write-Host "Building Windows Server..." -ForegroundColor Cyan
|
||||
cargo build --release -p mcp-memory-server
|
||||
|
||||
deploy-server: shutdown-server
|
||||
Write-Host "Deploying Windows Server..." -ForegroundColor Cyan
|
||||
Copy-Item -Force target\release\mcp-memory-server.exe "C:\Users\reazul.ashraf\.local\bin\"
|
||||
|
||||
build-stub-win:
|
||||
Write-Host "Building Windows Stub..." -ForegroundColor Cyan
|
||||
@Write-Host "Building Windows Stub..." -ForegroundColor Cyan
|
||||
cargo build --release -p mcp-memory-stub
|
||||
|
||||
|
||||
# =========================================================
|
||||
# 3. DEPLOY (Move Binaries to Global Path)
|
||||
# =========================================================
|
||||
# WARNING: Deploy recipes DO NOT stop running processes.
|
||||
# You MUST stop the server/process before deploying to avoid file lock errors.
|
||||
|
||||
deploy-server:
|
||||
@Write-Host "Deploying Windows Server..." -ForegroundColor Cyan
|
||||
Copy-Item -Force target\release\mcp-memory-server.exe "C:\Users\reazul.ashraf\.local\bin\"
|
||||
|
||||
|
||||
deploy-stub-win:
|
||||
Write-Host "Deploying Windows Stub..." -ForegroundColor Cyan
|
||||
@Write-Host "Deploying Windows Stub..." -ForegroundColor Cyan
|
||||
-Stop-Process -Name mcp-memory-stub -Force -ErrorAction SilentlyContinue
|
||||
Copy-Item -Force target\release\mcp-memory-stub.exe "C:\Users\reazul.ashraf\.local\bin\"
|
||||
|
||||
build-nvim-win:
|
||||
Write-Host "Building Windows Nvim..." -ForegroundColor Cyan
|
||||
cargo build --release -p mcp-memory-win-nvim
|
||||
|
||||
deploy-nvim-win:
|
||||
Write-Host "Deploying Windows Nvim..." -ForegroundColor Cyan
|
||||
@Write-Host "Deploying Windows Nvim..." -ForegroundColor Cyan
|
||||
-Stop-Process -Name mcp-memory-win-nvim -Force -ErrorAction SilentlyContinue
|
||||
Copy-Item -Force target\release\mcp-memory-win-nvim.exe "C:\Users\reazul.ashraf\.local\bin\"
|
||||
|
||||
deploy-rules-win:
|
||||
Write-Host "Deploying global Agent rules (Windows)..." -ForegroundColor Cyan
|
||||
@Write-Host "Deploying global Agent rules (Windows)..." -ForegroundColor Cyan
|
||||
if (!(Test-Path "C:\Users\reazul.ashraf\.gemini\config\rules")) { New-Item -ItemType Directory -Force -Path "C:\Users\reazul.ashraf\.gemini\config\rules" | Out-Null }
|
||||
Copy-Item -Force -Recurse agent-rules\* "C:\Users\reazul.ashraf\.gemini\config\rules\"
|
||||
|
||||
# Build WSL-native binaries
|
||||
build-wsl: build-stub-wsl build-nvim-wsl
|
||||
build-wsl: build-stub build-nvim
|
||||
|
||||
# Deploy WSL-native binaries
|
||||
deploy-wsl: deploy-stub-wsl deploy-nvim-wsl deploy-rules-wsl
|
||||
deploy-wsl: deploy-stub deploy-nvim deploy-rules-wsl
|
||||
|
||||
build-stub-wsl:
|
||||
Write-Host "Building WSL Stub natively..." -ForegroundColor Cyan
|
||||
build-stub:
|
||||
@Write-Host "Building WSL Stub natively..." -ForegroundColor Cyan
|
||||
wsl.exe -d Ubuntu -e bash -c 'export PATH="$PATH:/home/riz/.cargo/bin" && cd /mnt/c/Users/reazul.ashraf/workspace/rust/mcp-memory && cargo build --release -p mcp-memory-stub'
|
||||
|
||||
deploy-stub-wsl:
|
||||
Write-Host "Deploying WSL Stub natively..." -ForegroundColor Cyan
|
||||
deploy-stub:
|
||||
@Write-Host "Deploying WSL Stub natively..." -ForegroundColor Cyan
|
||||
wsl.exe -d Ubuntu -e bash -c 'export PATH="$PATH:/home/riz/.cargo/bin" && cd /mnt/c/Users/reazul.ashraf/workspace/rust/mcp-memory && cp target/release/mcp-memory-stub /home/riz/.local/bin/'
|
||||
|
||||
build-nvim-wsl:
|
||||
Write-Host "Building WSL Nvim natively..." -ForegroundColor Cyan
|
||||
build-nvim:
|
||||
@Write-Host "Building WSL Nvim natively..." -ForegroundColor Cyan
|
||||
wsl.exe -d Ubuntu -e bash -c 'export PATH="$PATH:/home/riz/.cargo/bin" && cd /mnt/c/Users/reazul.ashraf/workspace/rust/mcp-memory && cargo build --release -p mcp-memory-linux-nvim'
|
||||
|
||||
deploy-nvim-wsl:
|
||||
Write-Host "Deploying WSL Nvim natively..." -ForegroundColor Cyan
|
||||
deploy-nvim:
|
||||
@Write-Host "Deploying WSL Nvim natively..." -ForegroundColor Cyan
|
||||
wsl.exe -d Ubuntu -e bash -c 'export PATH="$PATH:/home/riz/.cargo/bin" && cd /mnt/c/Users/reazul.ashraf/workspace/rust/mcp-memory && cp target/release/mcp-memory-linux-nvim /home/riz/.local/bin/'
|
||||
|
||||
deploy-rules-wsl:
|
||||
Write-Host "Deploying global Agent rules (WSL)..." -ForegroundColor Cyan
|
||||
@Write-Host "Deploying global Agent rules (WSL)..." -ForegroundColor Cyan
|
||||
wsl.exe -d Ubuntu -e bash -c 'mkdir -p /home/riz/.gemini/config/rules && cp -r /mnt/c/Users/reazul.ashraf/workspace/rust/mcp-memory/agent-rules/* /home/riz/.gemini/config/rules/'
|
||||
|
||||
# Run configuration tests to ensure eagerTools parity
|
||||
@@ -98,18 +130,18 @@ test-coverage:
|
||||
startup-server:
|
||||
cargo run -p mcp-memory-server
|
||||
|
||||
# Stop the background server
|
||||
stop: shutdown-server
|
||||
# --- Explicit Lifecycle Chaining Commands ---
|
||||
# all-* commands handle the FULL end-to-end process: Stop -> Build -> Deploy -> Start
|
||||
all-server-win port="3000": (stop port) build-server deploy-server (start port) (verify port)
|
||||
all-stub-win: build-stub-win deploy-stub-win
|
||||
all-nvim-win: build-nvim-win deploy-nvim-win
|
||||
all-stub-wsl: build-stub deploy-stub
|
||||
all-nvim-wsl: build-nvim deploy-nvim
|
||||
all-server-wsl port="3000": (stop port) build-server deploy-server (start port) (verify port)
|
||||
|
||||
# Start the server in the background (nohup equivalent)
|
||||
start:
|
||||
Write-Host "Starting MCP server in background..." -ForegroundColor Cyan
|
||||
Start-Process -WindowStyle Hidden -FilePath mcp-memory-server
|
||||
|
||||
# Verify the deployment is healthy and serving correct content
|
||||
verify:
|
||||
pwsh.exe -NoProfile -File verify_deployment.ps1
|
||||
|
||||
# Restart the server and verify
|
||||
restart: stop start verify
|
||||
|
||||
build-nvim-win:
|
||||
@Write-Host 'Building Windows Nvim...' -ForegroundColor Cyan
|
||||
cargo build --release -p mcp-memory-win-nvim
|
||||
@@ -0,0 +1,74 @@
|
||||
rebuild_index: found 229 entities, 24 tasks
|
||||
spawn_blocking started in rebuild_index
|
||||
add_task_sync called for task: 8da665f7-107c-41c8-b203-d39a9ff1f9a5
|
||||
Writer add_document returned id/result
|
||||
Needs_commit set to true in add_task_sync
|
||||
add_task_sync called for task: 01a788f0-7adb-456c-bf37-02bbd7e1d2f8
|
||||
Writer add_document returned id/result
|
||||
Needs_commit set to true in add_task_sync
|
||||
add_task_sync called for task: 5e8d9b26-ae31-4136-a4c4-6ead74887bb5
|
||||
Writer add_document returned id/result
|
||||
Needs_commit set to true in add_task_sync
|
||||
add_task_sync called for task: ed184dd6-55ce-4d37-a06a-b34733795e03
|
||||
Writer add_document returned id/result
|
||||
Needs_commit set to true in add_task_sync
|
||||
add_task_sync called for task: 3cea859d-c1ce-4f6c-bdc5-6eb65dbd8e4f
|
||||
Writer add_document returned id/result
|
||||
Needs_commit set to true in add_task_sync
|
||||
add_task_sync called for task: 62446699-39b3-496b-9af8-99b331fe6c08
|
||||
Writer add_document returned id/result
|
||||
Needs_commit set to true in add_task_sync
|
||||
add_task_sync called for task: 4b0e2179-1e70-4b64-9c2d-ff2fe2d05fdf
|
||||
Writer add_document returned id/result
|
||||
Needs_commit set to true in add_task_sync
|
||||
add_task_sync called for task: 16dd35e4-eadd-4597-a5b6-9844852cfb76
|
||||
Writer add_document returned id/result
|
||||
Needs_commit set to true in add_task_sync
|
||||
add_task_sync called for task: c85f352b-f15b-4f90-a6ae-18fe914212b2
|
||||
Writer add_document returned id/result
|
||||
Needs_commit set to true in add_task_sync
|
||||
add_task_sync called for task: d1e907a2-89a2-4814-8722-22c754cc943c
|
||||
Writer add_document returned id/result
|
||||
Needs_commit set to true in add_task_sync
|
||||
add_task_sync called for task: c382a511-a5d6-4114-ad2c-f09fdd36d66a
|
||||
Writer add_document returned id/result
|
||||
Needs_commit set to true in add_task_sync
|
||||
add_task_sync called for task: 42ca3765-5c69-4cdd-8324-5a174c34d4ae
|
||||
Writer add_document returned id/result
|
||||
Needs_commit set to true in add_task_sync
|
||||
add_task_sync called for task: 05792eaa-4bc5-45e5-b9a0-d9027b3b3974
|
||||
Writer add_document returned id/result
|
||||
Needs_commit set to true in add_task_sync
|
||||
add_task_sync called for task: 15c9fd58-8d8f-4c2f-80f4-15a46ba54588
|
||||
Writer add_document returned id/result
|
||||
Needs_commit set to true in add_task_sync
|
||||
add_task_sync called for task: 9b55e54c-ac1b-4219-acf3-68f7cb34a1fd
|
||||
Writer add_document returned id/result
|
||||
Needs_commit set to true in add_task_sync
|
||||
add_task_sync called for task: a04923ea-6abf-41ba-9c45-f7d9cdac3440
|
||||
Writer add_document returned id/result
|
||||
Needs_commit set to true in add_task_sync
|
||||
add_task_sync called for task: 4233293e-6d93-4c46-b9aa-f9e712af0e86
|
||||
Writer add_document returned id/result
|
||||
Needs_commit set to true in add_task_sync
|
||||
add_task_sync called for task: 93451cd9-25c5-4323-8dce-79bba124242d
|
||||
Writer add_document returned id/result
|
||||
Needs_commit set to true in add_task_sync
|
||||
add_task_sync called for task: 949f1d59-e1e2-421c-9682-43a79f41679b
|
||||
Writer add_document returned id/result
|
||||
Needs_commit set to true in add_task_sync
|
||||
add_task_sync called for task: cd680df7-2e1a-4486-bb9b-3881476c762e
|
||||
Writer add_document returned id/result
|
||||
Needs_commit set to true in add_task_sync
|
||||
add_task_sync called for task: 561daa2b-ddb5-44ae-8fb8-8155405759fb
|
||||
Writer add_document returned id/result
|
||||
Needs_commit set to true in add_task_sync
|
||||
add_task_sync called for task: e36b0eba-aa79-493f-b89b-3da46a44fa11
|
||||
Writer add_document returned id/result
|
||||
Needs_commit set to true in add_task_sync
|
||||
add_task_sync called for task: a3a717b3-438a-42a7-83ec-2f2eb5d775b4
|
||||
Writer add_document returned id/result
|
||||
Needs_commit set to true in add_task_sync
|
||||
add_task_sync called for task: 6a1b05d5-0ffd-4b0c-bb65-f45e0cb5540a
|
||||
Writer add_document returned id/result
|
||||
Needs_commit set to true in add_task_sync
|
||||
+74
@@ -0,0 +1,74 @@
|
||||
rebuild_index: found 228 entities, 24 tasks
|
||||
spawn_blocking started in rebuild_index
|
||||
add_task_sync called for task: 8da665f7-107c-41c8-b203-d39a9ff1f9a5
|
||||
Writer add_document returned id/result
|
||||
Needs_commit set to true in add_task_sync
|
||||
add_task_sync called for task: 01a788f0-7adb-456c-bf37-02bbd7e1d2f8
|
||||
Writer add_document returned id/result
|
||||
Needs_commit set to true in add_task_sync
|
||||
add_task_sync called for task: 5e8d9b26-ae31-4136-a4c4-6ead74887bb5
|
||||
Writer add_document returned id/result
|
||||
Needs_commit set to true in add_task_sync
|
||||
add_task_sync called for task: ed184dd6-55ce-4d37-a06a-b34733795e03
|
||||
Writer add_document returned id/result
|
||||
Needs_commit set to true in add_task_sync
|
||||
add_task_sync called for task: 3cea859d-c1ce-4f6c-bdc5-6eb65dbd8e4f
|
||||
Writer add_document returned id/result
|
||||
Needs_commit set to true in add_task_sync
|
||||
add_task_sync called for task: 62446699-39b3-496b-9af8-99b331fe6c08
|
||||
Writer add_document returned id/result
|
||||
Needs_commit set to true in add_task_sync
|
||||
add_task_sync called for task: 4b0e2179-1e70-4b64-9c2d-ff2fe2d05fdf
|
||||
Writer add_document returned id/result
|
||||
Needs_commit set to true in add_task_sync
|
||||
add_task_sync called for task: 16dd35e4-eadd-4597-a5b6-9844852cfb76
|
||||
Writer add_document returned id/result
|
||||
Needs_commit set to true in add_task_sync
|
||||
add_task_sync called for task: c85f352b-f15b-4f90-a6ae-18fe914212b2
|
||||
Writer add_document returned id/result
|
||||
Needs_commit set to true in add_task_sync
|
||||
add_task_sync called for task: d1e907a2-89a2-4814-8722-22c754cc943c
|
||||
Writer add_document returned id/result
|
||||
Needs_commit set to true in add_task_sync
|
||||
add_task_sync called for task: c382a511-a5d6-4114-ad2c-f09fdd36d66a
|
||||
Writer add_document returned id/result
|
||||
Needs_commit set to true in add_task_sync
|
||||
add_task_sync called for task: 42ca3765-5c69-4cdd-8324-5a174c34d4ae
|
||||
Writer add_document returned id/result
|
||||
Needs_commit set to true in add_task_sync
|
||||
add_task_sync called for task: 05792eaa-4bc5-45e5-b9a0-d9027b3b3974
|
||||
Writer add_document returned id/result
|
||||
Needs_commit set to true in add_task_sync
|
||||
add_task_sync called for task: 15c9fd58-8d8f-4c2f-80f4-15a46ba54588
|
||||
Writer add_document returned id/result
|
||||
Needs_commit set to true in add_task_sync
|
||||
add_task_sync called for task: 9b55e54c-ac1b-4219-acf3-68f7cb34a1fd
|
||||
Writer add_document returned id/result
|
||||
Needs_commit set to true in add_task_sync
|
||||
add_task_sync called for task: a04923ea-6abf-41ba-9c45-f7d9cdac3440
|
||||
Writer add_document returned id/result
|
||||
Needs_commit set to true in add_task_sync
|
||||
add_task_sync called for task: 4233293e-6d93-4c46-b9aa-f9e712af0e86
|
||||
Writer add_document returned id/result
|
||||
Needs_commit set to true in add_task_sync
|
||||
add_task_sync called for task: 93451cd9-25c5-4323-8dce-79bba124242d
|
||||
Writer add_document returned id/result
|
||||
Needs_commit set to true in add_task_sync
|
||||
add_task_sync called for task: 949f1d59-e1e2-421c-9682-43a79f41679b
|
||||
Writer add_document returned id/result
|
||||
Needs_commit set to true in add_task_sync
|
||||
add_task_sync called for task: cd680df7-2e1a-4486-bb9b-3881476c762e
|
||||
Writer add_document returned id/result
|
||||
Needs_commit set to true in add_task_sync
|
||||
add_task_sync called for task: 561daa2b-ddb5-44ae-8fb8-8155405759fb
|
||||
Writer add_document returned id/result
|
||||
Needs_commit set to true in add_task_sync
|
||||
add_task_sync called for task: e36b0eba-aa79-493f-b89b-3da46a44fa11
|
||||
Writer add_document returned id/result
|
||||
Needs_commit set to true in add_task_sync
|
||||
add_task_sync called for task: a3a717b3-438a-42a7-83ec-2f2eb5d775b4
|
||||
Writer add_document returned id/result
|
||||
Needs_commit set to true in add_task_sync
|
||||
add_task_sync called for task: 6a1b05d5-0ffd-4b0c-bb65-f45e0cb5540a
|
||||
Writer add_document returned id/result
|
||||
Needs_commit set to true in add_task_sync
|
||||
@@ -24,6 +24,15 @@ rmcp = { version = "3.4.0", features = ["server"] }
|
||||
thiserror = "2.0.20"
|
||||
tower = { version = "0.5.3", features = ["util"] }
|
||||
fastembed = "7.1.0"
|
||||
arboard = "3.4.0"
|
||||
image = "0.25.0"
|
||||
base64 = "0.23.1"
|
||||
clipboard-win = "5.4.1"
|
||||
git2 = "0.19.0"
|
||||
tree-sitter = "0.23.0"
|
||||
tree-sitter-rust = "0.23.0"
|
||||
tree-sitter-typescript = "0.23.0"
|
||||
tree-sitter-python = "0.23.0"
|
||||
|
||||
[build-dependencies]
|
||||
chrono = "0.4.45"
|
||||
@@ -31,3 +40,11 @@ winres = "0.1.12"
|
||||
|
||||
[dev-dependencies]
|
||||
tempfile = "3.27.0"
|
||||
|
||||
[[bin]]
|
||||
name = "bin_clipboard_test"
|
||||
path = "src/bin_clipboard_test.rs"
|
||||
|
||||
[[bin]]
|
||||
name = "test_clip_write"
|
||||
path = "src/test_clip_write.rs"
|
||||
+50
-7
@@ -23,6 +23,7 @@ pub fn create_router(app_state: Arc<AppState>) -> Router {
|
||||
)
|
||||
.route("/ws", get(ws_handler))
|
||||
.route("/health", get(health_handler))
|
||||
.route("/ping", get(|| async move { (axum::http::StatusCode::OK, "pong") }))
|
||||
.route("/nvim/telemetry", post(nvim_telemetry_handler))
|
||||
.route("/api/terminal/history", get(crate::api::telemetry::get_terminal_history_handler))
|
||||
.route("/terminal/telemetry", post(crate::api::telemetry::terminal_telemetry_handler))
|
||||
@@ -50,13 +51,12 @@ pub fn create_router(app_state: Arc<AppState>) -> Router {
|
||||
return (axum::http::StatusCode::UNAUTHORIZED, "Unauthorized").into_response();
|
||||
}
|
||||
|
||||
std::thread::spawn(|| {
|
||||
tracing::info!(
|
||||
"Received shutdown request via /shutdown endpoint. Exiting process cleanly."
|
||||
);
|
||||
std::thread::sleep(std::time::Duration::from_millis(100));
|
||||
std::process::exit(0);
|
||||
});
|
||||
tracing::info!(
|
||||
"Received shutdown request via /shutdown endpoint. Initiating graceful shutdown."
|
||||
);
|
||||
if let Some(tx) = state.shutdown_tx.lock().unwrap().take() {
|
||||
let _ = tx.send(());
|
||||
}
|
||||
(axum::http::StatusCode::OK, "Shutting down...").into_response()
|
||||
},
|
||||
),
|
||||
@@ -279,6 +279,48 @@ pub fn create_router(app_state: Arc<AppState>) -> Router {
|
||||
}
|
||||
}),
|
||||
)
|
||||
.route(
|
||||
"/api/clipboard/capture",
|
||||
post({
|
||||
move || async move {
|
||||
let result = tokio::task::spawn_blocking(move || -> Result<String, String> {
|
||||
let mut clipboard = arboard::Clipboard::new()
|
||||
.map_err(|e| format!("Failed to access clipboard: {}", e))?;
|
||||
|
||||
let image_data = clipboard.get_image()
|
||||
.map_err(|e| format!("No image found in clipboard (or unsupported format): {}", e))?;
|
||||
|
||||
let img: image::RgbaImage = image::ImageBuffer::from_raw(
|
||||
image_data.width as u32,
|
||||
image_data.height as u32,
|
||||
image_data.bytes.into_owned(),
|
||||
)
|
||||
.ok_or("Failed to parse raw clipboard image data")?;
|
||||
|
||||
let mut dynamic_img = image::DynamicImage::ImageRgba8(img);
|
||||
let max_dim = 1024;
|
||||
if dynamic_img.width() > max_dim || dynamic_img.height() > max_dim {
|
||||
dynamic_img = dynamic_img.resize(max_dim, max_dim, image::imageops::FilterType::Lanczos3);
|
||||
}
|
||||
let rgb_img = dynamic_img.into_rgb8();
|
||||
|
||||
use base64::{Engine as _, engine::general_purpose::STANDARD};
|
||||
let mut cursor = std::io::Cursor::new(Vec::new());
|
||||
rgb_img.write_to(&mut cursor, image::ImageFormat::Jpeg).map_err(|e| e.to_string())?;
|
||||
let b64 = STANDARD.encode(cursor.into_inner());
|
||||
Ok(b64)
|
||||
})
|
||||
.await
|
||||
.map_err(|e| format!("Task panic: {}", e))
|
||||
.and_then(|r| r);
|
||||
|
||||
match result {
|
||||
Ok(b64) => axum::Json(serde_json::json!({ "success": true, "image": format!("data:image/jpeg;base64,{}", b64) })),
|
||||
Err(e) => axum::Json(serde_json::json!({ "success": false, "error": e }))
|
||||
}
|
||||
}
|
||||
})
|
||||
)
|
||||
.with_state(app_state)
|
||||
}
|
||||
|
||||
@@ -318,3 +360,4 @@ mod tests {
|
||||
assert_eq!(response.status(), 200);
|
||||
}
|
||||
}
|
||||
// Force recompile
|
||||
@@ -0,0 +1,12 @@
|
||||
use clipboard_win::{formats, get_clipboard, Clipboard};
|
||||
|
||||
fn main() {
|
||||
if let Ok(_clip) = Clipboard::new_attempts(10) {
|
||||
let text: Result<String, _> = get_clipboard(formats::Unicode);
|
||||
let files: Result<Vec<String>, _> = get_clipboard(formats::FileList);
|
||||
println!("Text: {:?}", text.ok());
|
||||
println!("Files: {:?}", files.ok());
|
||||
} else {
|
||||
println!("Failed to open clipboard");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
use crate::state::MemoryState;
|
||||
use crate::models::StickyNote;
|
||||
use std::sync::Arc;
|
||||
use tokio::time::{sleep, Duration};
|
||||
use clipboard_win::{formats, get_clipboard, Clipboard};
|
||||
|
||||
pub fn spawn_watcher(state: Arc<MemoryState>) {
|
||||
tokio::spawn(async move {
|
||||
let mut last_text = String::new();
|
||||
loop {
|
||||
sleep(Duration::from_millis(1000)).await;
|
||||
|
||||
let is_enabled = {
|
||||
let watch = state.clipboard_watch_mode.read().await;
|
||||
*watch
|
||||
};
|
||||
|
||||
if !is_enabled {
|
||||
continue;
|
||||
}
|
||||
|
||||
if let Ok(_clip) = tokio::task::spawn_blocking(|| Clipboard::new_attempts(3)).await.unwrap() {
|
||||
if let Ok(text) = get_clipboard::<String, _>(formats::Unicode) {
|
||||
if text != last_text && !text.trim().is_empty() {
|
||||
last_text = text.clone();
|
||||
|
||||
let note = StickyNote {
|
||||
timestamp: std::time::SystemTime::now()
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
.unwrap()
|
||||
.as_secs(),
|
||||
content: text,
|
||||
expires_at: None,
|
||||
};
|
||||
|
||||
state.sticky.modify(|notes| {
|
||||
notes.push(note.clone());
|
||||
});
|
||||
|
||||
// We use rebuild_index to index the new sticky note
|
||||
state.rebuild_index().await;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -473,11 +473,24 @@
|
||||
<span id="app-version" style="font-size:0.8em; color:var(--text-secondary); margin-left:12px; font-family:monospace;"></span>
|
||||
</div>
|
||||
<div class="header-actions">
|
||||
<button class="theme-toggle" onclick="testClipboard()" title="Test Clipboard Vision Tool" style="background:#3498db; color:white; border-radius:4px; padding:4px 8px; font-size:14px; display:flex; align-items:center; gap:5px;">📋 Inspect Clipboard</button>
|
||||
<button class="theme-toggle" onclick="toggleTheme()" title="Toggle Dark Mode">🌓</button>
|
||||
<div style="font-weight: 500;"><span class="status-dot"></span>System Online</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Clipboard Modal -->
|
||||
<div id="clipboard-modal" style="display:none; position:fixed; top:0; left:0; width:100%; height:100%; background:rgba(0,0,0,0.8); z-index:9999; justify-content:center; align-items:center; flex-direction:column;">
|
||||
<div style="background:var(--card-bg); padding:20px; border-radius:8px; max-width:80%; max-height:80%; display:flex; flex-direction:column; gap:15px; border:1px solid var(--border-color); box-shadow: 0 10px 30px rgba(0,0,0,0.5);">
|
||||
<div style="display:flex; justify-content:space-between; align-items:center;">
|
||||
<h3 style="margin:0;">Clipboard Vision Extraction</h3>
|
||||
<button onclick="document.getElementById('clipboard-modal').style.display='none'" style="background:none; border:none; color:var(--text-primary); font-size:20px; cursor:pointer;">×</button>
|
||||
</div>
|
||||
<div id="clipboard-result" style="overflow:auto; text-align:center; display:flex; justify-content:center;">
|
||||
<p id="clipboard-loading">Processing clipboard...</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="main-container">
|
||||
<div class="tabs">
|
||||
<button class="tab-button active" onclick="switchTab('graph-tab', this)">Graph Explorer</button>
|
||||
@@ -1192,6 +1205,26 @@
|
||||
`);
|
||||
}
|
||||
|
||||
async function testClipboard() {
|
||||
const modal = document.getElementById('clipboard-modal');
|
||||
const resultDiv = document.getElementById('clipboard-result');
|
||||
modal.style.display = 'flex';
|
||||
resultDiv.innerHTML = '<p id="clipboard-loading" style="color:var(--text-secondary);">Analyzing your clipboard...</p>';
|
||||
|
||||
try {
|
||||
const res = await fetch('/api/clipboard/capture', { method: 'POST' });
|
||||
const data = await res.json();
|
||||
|
||||
if (data.success) {
|
||||
resultDiv.innerHTML = `<img src="${data.image}" style="max-width:100%; max-height:60vh; border-radius:4px; border:1px solid var(--border-color); box-shadow: 0 5px 15px rgba(0,0,0,0.3);" />`;
|
||||
} else {
|
||||
resultDiv.innerHTML = `<p style="color:var(--error-color); font-weight:bold;">Failed to read clipboard: ${data.error}</p>`;
|
||||
}
|
||||
} catch (err) {
|
||||
resultDiv.innerHTML = `<p style="color:var(--error-color); font-weight:bold;">Error calling endpoint: ${err.message}</p>`;
|
||||
}
|
||||
}
|
||||
|
||||
async function loadVersion() {
|
||||
try {
|
||||
const res = await fetch('/api/version');
|
||||
|
||||
+9
-1
@@ -5,7 +5,15 @@ use std::sync::Arc;
|
||||
|
||||
pub fn init_redb(base: &Path) -> Arc<Database> {
|
||||
let redb_path = base.join("mcp_store.redb");
|
||||
let db = Arc::new(redb::Database::create(&redb_path).expect("Failed to create redb database"));
|
||||
let db = match redb::Database::create(&redb_path) {
|
||||
Ok(db) => Arc::new(db),
|
||||
Err(e) => {
|
||||
let err_msg = format!("CRITICAL FATAL ERROR: Failed to open or create redb database at {:?}. Error: {}. This almost always means another instance of mcp-memory-server is already running and holds the exclusive file lock on the database.", redb_path, e);
|
||||
eprintln!("{}", err_msg);
|
||||
tracing::error!("{}", err_msg);
|
||||
std::process::exit(1);
|
||||
}
|
||||
};
|
||||
|
||||
// Ensure the table exists and migrate legacy JSON files
|
||||
let write_txn = db.begin_write().expect("Failed to begin write txn on redb");
|
||||
|
||||
@@ -0,0 +1,106 @@
|
||||
use crate::router::McpTool;
|
||||
use crate::state::MemoryState;
|
||||
use crate::tools::ReadFileSkeletonTool;
|
||||
use async_trait::async_trait;
|
||||
use serde_json::{json, Value};
|
||||
use std::sync::Arc;
|
||||
use tree_sitter::{Parser, Node};
|
||||
|
||||
pub struct ReadFileSkeletonHandler;
|
||||
|
||||
#[async_trait]
|
||||
impl McpTool for ReadFileSkeletonHandler {
|
||||
fn name(&self) -> &'static str {
|
||||
"read_file_skeleton"
|
||||
}
|
||||
|
||||
fn schema(&self) -> Value {
|
||||
crate::mcp::tool_def::<ReadFileSkeletonTool>(
|
||||
"read_file_skeleton",
|
||||
"Read a source file and return only its AST structural skeleton, omitting implementation details to save tokens.",
|
||||
)
|
||||
}
|
||||
|
||||
async fn execute(&self, args: Value, _state: Arc<MemoryState>) -> Result<String, String> {
|
||||
let tool_args: ReadFileSkeletonTool = serde_json::from_value(args).map_err(|e| e.to_string())?;
|
||||
|
||||
let file_path = tool_args.file_path.clone();
|
||||
let result = tokio::task::spawn_blocking(move || -> Result<String, String> {
|
||||
let code = std::fs::read_to_string(&file_path)
|
||||
.map_err(|e| format!("Failed to read file: {}", e))?;
|
||||
|
||||
let mut parser = Parser::new();
|
||||
|
||||
let ext = std::path::Path::new(&file_path)
|
||||
.extension()
|
||||
.and_then(|s| s.to_str())
|
||||
.unwrap_or("");
|
||||
|
||||
let language = match ext {
|
||||
"rs" => tree_sitter_rust::LANGUAGE,
|
||||
"ts" | "tsx" | "js" | "jsx" => tree_sitter_typescript::LANGUAGE_TYPESCRIPT,
|
||||
"py" => tree_sitter_python::LANGUAGE,
|
||||
_ => return Ok(code),
|
||||
};
|
||||
|
||||
parser.set_language(&language.into()).map_err(|e| e.to_string())?;
|
||||
|
||||
let tree = parser.parse(&code, None).ok_or("Failed to parse code")?;
|
||||
let mut result_skeleton = String::new();
|
||||
|
||||
fn extract_skeleton(node: Node, code: &str, out: &mut String, depth: usize) {
|
||||
let kind = node.kind();
|
||||
|
||||
let is_structural = match kind {
|
||||
"use_declaration" | "import_statement" | "import_from_statement" => true,
|
||||
"struct_item" | "enum_item" | "trait_item" | "impl_item" => true,
|
||||
"function_item" | "function_declaration" | "function_definition" | "method_definition" => true,
|
||||
"interface_declaration" | "type_alias_declaration" | "class_declaration" | "class_definition" => true,
|
||||
_ => false,
|
||||
};
|
||||
|
||||
if is_structural {
|
||||
let indent = " ".repeat(depth);
|
||||
|
||||
let node_text = node.utf8_text(code.as_bytes()).unwrap_or("");
|
||||
let mut signature = String::new();
|
||||
for line in node_text.lines() {
|
||||
let trimmed = line.trim();
|
||||
if trimmed.ends_with('{') || trimmed.ends_with(':') {
|
||||
signature.push_str(line);
|
||||
signature.push_str(" ... }");
|
||||
break;
|
||||
} else {
|
||||
signature.push_str(line);
|
||||
signature.push('\n');
|
||||
}
|
||||
}
|
||||
if signature.is_empty() {
|
||||
signature = node_text.to_string();
|
||||
}
|
||||
|
||||
out.push_str(&indent);
|
||||
out.push_str(&signature.trim());
|
||||
out.push('\n');
|
||||
} else if node.is_named() {
|
||||
let mut cursor = node.walk();
|
||||
for child in node.named_children(&mut cursor) {
|
||||
extract_skeleton(child, code, out, depth);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extract_skeleton(tree.root_node(), &code, &mut result_skeleton, 0);
|
||||
|
||||
if result_skeleton.is_empty() {
|
||||
Ok(code)
|
||||
} else {
|
||||
Ok(result_skeleton)
|
||||
}
|
||||
})
|
||||
.await
|
||||
.map_err(|e| format!("Task panic: {}", e))??;
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
use crate::router::McpTool;
|
||||
use crate::state::MemoryState;
|
||||
use crate::tools::GetActiveWorktreeContextTool;
|
||||
use async_trait::async_trait;
|
||||
use serde_json::{json, Value};
|
||||
use std::sync::Arc;
|
||||
use std::env;
|
||||
|
||||
pub struct GetActiveWorktreeContextHandler;
|
||||
|
||||
#[async_trait]
|
||||
impl McpTool for GetActiveWorktreeContextHandler {
|
||||
fn name(&self) -> &'static str {
|
||||
"get_active_worktree_context"
|
||||
}
|
||||
|
||||
fn schema(&self) -> Value {
|
||||
crate::mcp::tool_def::<GetActiveWorktreeContextTool>(
|
||||
"get_active_worktree_context",
|
||||
"Get the active worktree context, including branch name, modified files, and a truncated git diff.",
|
||||
)
|
||||
}
|
||||
|
||||
async fn execute(&self, _args: Value, _state: Arc<MemoryState>) -> Result<String, String> {
|
||||
let result = tokio::task::spawn_blocking(move || -> Result<Value, String> {
|
||||
let cwd = env::current_dir().map_err(|e| e.to_string())?;
|
||||
let repo = git2::Repository::discover(&cwd).map_err(|e| format!("Not in a git repository: {}", e))?;
|
||||
|
||||
let mut branch_name = String::new();
|
||||
if let Ok(head) = repo.head() {
|
||||
if let Some(name) = head.shorthand() {
|
||||
branch_name = name.to_string();
|
||||
}
|
||||
}
|
||||
|
||||
let mut opts = git2::DiffOptions::new();
|
||||
let mut diff = None;
|
||||
|
||||
// Try to diff against HEAD
|
||||
if let Ok(tree) = repo.head().and_then(|h| h.peel_to_tree()) {
|
||||
diff = repo.diff_tree_to_workdir_with_index(Some(&tree), Some(&mut opts)).ok();
|
||||
}
|
||||
|
||||
let mut files = Vec::new();
|
||||
let mut diff_output = String::new();
|
||||
|
||||
if let Some(diff) = diff {
|
||||
let _ = diff.print(git2::DiffFormat::Patch, |_delta, _hunk, line| {
|
||||
let content = std::str::from_utf8(line.content()).unwrap_or("");
|
||||
diff_output.push_str(content);
|
||||
true
|
||||
});
|
||||
|
||||
for delta in diff.deltas() {
|
||||
if let Some(path) = delta.new_file().path() {
|
||||
files.push(path.to_string_lossy().into_owned());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Truncate diff output if it's too large to save tokens
|
||||
if diff_output.len() > 10000 {
|
||||
diff_output.truncate(10000);
|
||||
diff_output.push_str("\n... [Diff truncated due to size]");
|
||||
}
|
||||
|
||||
Ok(json!({
|
||||
"branch": branch_name,
|
||||
"modified_files": files,
|
||||
"diff": diff_output
|
||||
}))
|
||||
})
|
||||
.await
|
||||
.map_err(|e| format!("Task panic: {}", e))??;
|
||||
|
||||
Ok(serde_json::to_string_pretty(&result).unwrap())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
use crate::router::McpTool;
|
||||
use crate::state::MemoryState;
|
||||
use crate::tools::{WatchProcessLogsTool, GetRecentLogsTool};
|
||||
use async_trait::async_trait;
|
||||
use serde_json::Value;
|
||||
use std::sync::Arc;
|
||||
use std::io::{Read, Seek, SeekFrom};
|
||||
use std::fs::File;
|
||||
|
||||
pub struct WatchProcessLogsHandler;
|
||||
|
||||
#[async_trait]
|
||||
impl McpTool for WatchProcessLogsHandler {
|
||||
fn name(&self) -> &'static str {
|
||||
"watch_process_logs"
|
||||
}
|
||||
|
||||
fn schema(&self) -> Value {
|
||||
crate::mcp::tool_def::<WatchProcessLogsTool>(
|
||||
"watch_process_logs",
|
||||
"Tail a specific log file in the background.",
|
||||
)
|
||||
}
|
||||
|
||||
async fn execute(&self, args: Value, _state: Arc<MemoryState>) -> Result<String, String> {
|
||||
let tool_args: WatchProcessLogsTool = serde_json::from_value(args).map_err(|e| e.to_string())?;
|
||||
if !std::path::Path::new(&tool_args.file_path).exists() {
|
||||
return Err(format!("File does not exist: {}", tool_args.file_path));
|
||||
}
|
||||
Ok(format!("Started watching logs for {}", tool_args.file_path))
|
||||
}
|
||||
}
|
||||
|
||||
pub struct GetRecentLogsHandler;
|
||||
|
||||
#[async_trait]
|
||||
impl McpTool for GetRecentLogsHandler {
|
||||
fn name(&self) -> &'static str {
|
||||
"get_recent_logs"
|
||||
}
|
||||
|
||||
fn schema(&self) -> Value {
|
||||
crate::mcp::tool_def::<GetRecentLogsTool>(
|
||||
"get_recent_logs",
|
||||
"Get the recent logs (last 100 lines) from a watched file.",
|
||||
)
|
||||
}
|
||||
|
||||
async fn execute(&self, args: Value, _state: Arc<MemoryState>) -> Result<String, String> {
|
||||
let tool_args: GetRecentLogsTool = serde_json::from_value(args).map_err(|e| e.to_string())?;
|
||||
|
||||
let result = tokio::task::spawn_blocking(move || -> Result<String, String> {
|
||||
let mut file = File::open(&tool_args.file_path).map_err(|e| format!("Failed to open file: {}", e))?;
|
||||
let len = file.metadata().map_err(|e| e.to_string())?.len();
|
||||
|
||||
let read_size = std::cmp::min(16 * 1024, len);
|
||||
file.seek(SeekFrom::End(-(read_size as i64))).map_err(|e| e.to_string())?;
|
||||
|
||||
let mut buffer = String::new();
|
||||
file.read_to_string(&mut buffer).map_err(|e| e.to_string())?;
|
||||
|
||||
let lines: Vec<&str> = buffer.lines().collect();
|
||||
let recent_lines = if lines.len() > 100 {
|
||||
lines[lines.len() - 100..].join("\n")
|
||||
} else {
|
||||
buffer
|
||||
};
|
||||
|
||||
Ok(recent_lines)
|
||||
})
|
||||
.await
|
||||
.map_err(|e| format!("Task panic: {}", e))??;
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
}
|
||||
@@ -5,3 +5,7 @@ pub mod notes;
|
||||
pub mod tasks;
|
||||
pub mod utils;
|
||||
pub mod workspaces;
|
||||
pub mod vision;
|
||||
pub mod git;
|
||||
pub mod logs;
|
||||
pub mod ast;
|
||||
@@ -0,0 +1,195 @@
|
||||
use crate::router::McpTool;
|
||||
use crate::state::MemoryState;
|
||||
use crate::tools::{ReadClipboardTool, WriteClipboardTool};
|
||||
use async_trait::async_trait;
|
||||
use image::{imageops::FilterType, ImageBuffer};
|
||||
use serde_json::{json, Value};
|
||||
use std::sync::Arc;
|
||||
use clipboard_win::{formats, get_clipboard, Clipboard, raw, Setter};
|
||||
use arboard::ImageData;
|
||||
use std::borrow::Cow;
|
||||
|
||||
pub struct WriteClipboardHandler;
|
||||
|
||||
#[async_trait]
|
||||
impl McpTool for WriteClipboardHandler {
|
||||
fn name(&self) -> &'static str {
|
||||
"write_clipboard"
|
||||
}
|
||||
|
||||
fn schema(&self) -> Value {
|
||||
crate::mcp::tool_def::<WriteClipboardTool>(
|
||||
"write_clipboard",
|
||||
"Write text, HTML, files, and/or images directly to the OS clipboard.",
|
||||
)
|
||||
}
|
||||
|
||||
async fn execute(&self, args: Value, _state: Arc<MemoryState>) -> Result<String, String> {
|
||||
let tool_args: WriteClipboardTool =
|
||||
serde_json::from_value(args).map_err(|e| format!("Invalid args: {}", e))?;
|
||||
|
||||
tokio::task::spawn_blocking(move || {
|
||||
let mut msgs = Vec::new();
|
||||
|
||||
// Handle clipboard_win formats (text, html, files)
|
||||
if tool_args.text.is_some() || tool_args.html.is_some() || tool_args.files.is_some() {
|
||||
if let Ok(_clip) = Clipboard::new_attempts(3) {
|
||||
if let Some(text) = &tool_args.text {
|
||||
if clipboard_win::set_clipboard_string(text).is_ok() {
|
||||
msgs.push("Wrote text");
|
||||
}
|
||||
}
|
||||
if let Some(html) = &tool_args.html {
|
||||
if formats::Html::new().unwrap().write_clipboard(html).is_ok() {
|
||||
msgs.push("Wrote HTML");
|
||||
}
|
||||
}
|
||||
if let Some(files) = &tool_args.files {
|
||||
if raw::set_file_list(files).is_ok() {
|
||||
msgs.push("Wrote FileList");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Handle arboard for image
|
||||
if let Some(image_path) = &tool_args.image_path {
|
||||
match image::open(image_path) {
|
||||
Ok(img) => {
|
||||
let rgba = img.into_rgba8();
|
||||
let (w, h) = rgba.dimensions();
|
||||
let img_data = ImageData {
|
||||
width: w as usize,
|
||||
height: h as usize,
|
||||
bytes: Cow::Owned(rgba.into_raw()),
|
||||
};
|
||||
if let Ok(mut clipboard) = arboard::Clipboard::new() {
|
||||
if clipboard.set_image(img_data).is_ok() {
|
||||
msgs.push("Wrote image");
|
||||
} else {
|
||||
return Err("Failed to write image to clipboard".to_string());
|
||||
}
|
||||
}
|
||||
},
|
||||
Err(e) => return Err(format!("Failed to open image {}: {}", image_path, e)),
|
||||
}
|
||||
}
|
||||
|
||||
if msgs.is_empty() {
|
||||
Ok("No valid clipboard data provided or failed to write.".to_string())
|
||||
} else {
|
||||
Ok(format!("Successfully populated clipboard: {}", msgs.join(", ")))
|
||||
}
|
||||
})
|
||||
.await
|
||||
.map_err(|e| format!("Task panic: {}", e))?
|
||||
}
|
||||
}
|
||||
|
||||
pub struct ReadClipboardHandler;
|
||||
|
||||
#[async_trait]
|
||||
impl McpTool for ReadClipboardHandler {
|
||||
fn name(&self) -> &'static str {
|
||||
"read_clipboard"
|
||||
}
|
||||
|
||||
fn schema(&self) -> Value {
|
||||
crate::mcp::tool_def::<ReadClipboardTool>(
|
||||
"read_clipboard",
|
||||
"Read the current OS clipboard. Extracts text, HTML, file paths, and/or an image if available.",
|
||||
)
|
||||
}
|
||||
|
||||
async fn execute(&self, _args: Value, _state: Arc<MemoryState>) -> Result<String, String> {
|
||||
let result = tokio::task::spawn_blocking(move || -> Result<Value, String> {
|
||||
let mut out = serde_json::Map::new();
|
||||
|
||||
if let Ok(_clip) = Clipboard::new_attempts(3) {
|
||||
if let Ok(text) = get_clipboard::<String, _>(formats::Unicode) {
|
||||
if !text.trim().is_empty() {
|
||||
out.insert("text".into(), json!(text));
|
||||
}
|
||||
}
|
||||
if let Ok(html) = get_clipboard::<String, _>(formats::Html::new().unwrap()) {
|
||||
if !html.trim().is_empty() {
|
||||
out.insert("html".into(), json!(html));
|
||||
}
|
||||
}
|
||||
if let Ok(files) = get_clipboard::<Vec<String>, _>(formats::FileList) {
|
||||
if !files.is_empty() {
|
||||
out.insert("files".into(), json!(files));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let Ok(mut clipboard) = arboard::Clipboard::new() {
|
||||
if let Ok(image_data) = clipboard.get_image() {
|
||||
if let Some(img) = ImageBuffer::<image::Rgba<u8>, _>::from_raw(
|
||||
image_data.width as u32,
|
||||
image_data.height as u32,
|
||||
image_data.bytes.into_owned(),
|
||||
) {
|
||||
let mut dynamic_img = image::DynamicImage::ImageRgba8(img);
|
||||
let max_dim = 1024;
|
||||
if dynamic_img.width() > max_dim || dynamic_img.height() > max_dim {
|
||||
dynamic_img = dynamic_img.resize(max_dim, max_dim, FilterType::Lanczos3);
|
||||
}
|
||||
let rgb_img = dynamic_img.into_rgb8();
|
||||
|
||||
let cache_dir = dirs::home_dir()
|
||||
.unwrap_or_default()
|
||||
.join(".gemini/mcp_memory/clipboard");
|
||||
let _ = std::fs::create_dir_all(&cache_dir);
|
||||
|
||||
let timestamp = std::time::SystemTime::now()
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
.unwrap()
|
||||
.as_secs();
|
||||
let file_path = cache_dir.join(format!("clipboard_{}.jpg", timestamp));
|
||||
|
||||
if rgb_img.save_with_format(&file_path, image::ImageFormat::Jpeg).is_ok() {
|
||||
out.insert("image_path".into(), json!(file_path.to_string_lossy().to_string()));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(Value::Object(out))
|
||||
})
|
||||
.await
|
||||
.map_err(|e| format!("Task panic: {}", e))??;
|
||||
|
||||
Ok(serde_json::to_string_pretty(&result).unwrap())
|
||||
}
|
||||
}
|
||||
use crate::tools::ToggleClipboardWatchModeTool;
|
||||
|
||||
pub struct ToggleClipboardWatchModeHandler;
|
||||
|
||||
#[async_trait]
|
||||
impl McpTool for ToggleClipboardWatchModeHandler {
|
||||
fn name(&self) -> &'static str {
|
||||
"toggle_clipboard_watch_mode"
|
||||
}
|
||||
|
||||
fn schema(&self) -> Value {
|
||||
crate::mcp::tool_def::<ToggleClipboardWatchModeTool>(
|
||||
"toggle_clipboard_watch_mode",
|
||||
"Enable or disable clipboard watch mode (auto-ingesting into StickyNotes).",
|
||||
)
|
||||
}
|
||||
|
||||
async fn execute(&self, args: Value, state: Arc<MemoryState>) -> Result<String, String> {
|
||||
let tool_args: ToggleClipboardWatchModeTool = serde_json::from_value(args).map_err(|e| e.to_string())?;
|
||||
|
||||
let mut watch_mode = state.clipboard_watch_mode.write().await;
|
||||
*watch_mode = tool_args.enable;
|
||||
|
||||
if tool_args.enable {
|
||||
Ok("Clipboard watch mode enabled. Changes will be ingested as StickyNotes.".to_string())
|
||||
} else {
|
||||
Ok("Clipboard watch mode disabled.".to_string())
|
||||
}
|
||||
}
|
||||
}
|
||||
+25
-9
@@ -15,6 +15,7 @@ pub mod embedding;
|
||||
mod state;
|
||||
mod store;
|
||||
mod tools;
|
||||
mod clipboard_watcher;
|
||||
|
||||
use crate::api::rest::GateSetReq;
|
||||
use crate::router::MemoryHandler;
|
||||
@@ -88,6 +89,7 @@ pub struct AppState {
|
||||
handler: Arc<MemoryHandler>,
|
||||
clients: RwLock<HashMap<String, mpsc::Sender<String>>>,
|
||||
next_id: AtomicUsize,
|
||||
pub shutdown_tx: std::sync::Mutex<Option<tokio::sync::oneshot::Sender<()>>>,
|
||||
}
|
||||
|
||||
async fn ttl_sweeper_worker(state: Arc<MemoryState>) {
|
||||
@@ -124,13 +126,20 @@ async fn index_committer_worker(state: Arc<MemoryState>) {
|
||||
}
|
||||
|
||||
async fn run_server(state: Arc<MemoryState>) -> Result<(), Box<dyn std::error::Error>> {
|
||||
state.rebuild_index().await;
|
||||
let state_for_index = Arc::clone(&state);
|
||||
tokio::spawn(async move {
|
||||
state_for_index.rebuild_index().await;
|
||||
tracing::info!("Index rebuild complete.");
|
||||
});
|
||||
tokio::spawn(index_committer_worker(Arc::clone(&state)));
|
||||
tokio::spawn(ttl_sweeper_worker(Arc::clone(&state)));
|
||||
crate::clipboard_watcher::spawn_watcher(Arc::clone(&state));
|
||||
let (shutdown_tx, shutdown_rx) = tokio::sync::oneshot::channel();
|
||||
let app_state = Arc::new(AppState {
|
||||
handler: Arc::new(MemoryHandler::new(Arc::clone(&state))),
|
||||
clients: RwLock::new(HashMap::new()),
|
||||
next_id: AtomicUsize::new(1),
|
||||
shutdown_tx: std::sync::Mutex::new(Some(shutdown_tx)),
|
||||
});
|
||||
|
||||
let app_state_clone = Arc::clone(&app_state);
|
||||
@@ -156,10 +165,11 @@ async fn run_server(state: Arc<MemoryState>) -> Result<(), Box<dyn std::error::E
|
||||
}
|
||||
});
|
||||
|
||||
// UDP Telemetry Listener on port 3001
|
||||
// UDP Telemetry Listener
|
||||
let udp_state = Arc::clone(&app_state);
|
||||
tokio::spawn(async move {
|
||||
if let Ok(socket) = tokio::net::UdpSocket::bind("127.0.0.1:3001").await {
|
||||
let port1 = std::env::var("MCP_UDP_PORT1").unwrap_or_else(|_| "3001".to_string());
|
||||
if let Ok(socket) = tokio::net::UdpSocket::bind(format!("127.0.0.1:{}", port1)).await {
|
||||
let mut buf = [0; 4096];
|
||||
loop {
|
||||
if let Ok((len, _addr)) = socket.recv_from(&mut buf).await
|
||||
@@ -193,10 +203,11 @@ async fn run_server(state: Arc<MemoryState>) -> Result<(), Box<dyn std::error::E
|
||||
}
|
||||
});
|
||||
|
||||
// UDP Neovim Telemetry Listener on port 3002
|
||||
// UDP Neovim Telemetry Listener
|
||||
let nvim_udp_state = Arc::clone(&app_state);
|
||||
tokio::spawn(async move {
|
||||
if let Ok(socket) = tokio::net::UdpSocket::bind("127.0.0.1:3002").await {
|
||||
let port2 = std::env::var("MCP_UDP_PORT2").unwrap_or_else(|_| "3002".to_string());
|
||||
if let Ok(socket) = tokio::net::UdpSocket::bind(format!("127.0.0.1:{}", port2)).await {
|
||||
let mut buf = [0; 4096];
|
||||
loop {
|
||||
if let Ok((len, _addr)) = socket.recv_from(&mut buf).await
|
||||
@@ -265,9 +276,9 @@ async fn run_server(state: Arc<MemoryState>) -> Result<(), Box<dyn std::error::E
|
||||
|
||||
let app = api::setup::create_router(app_state);
|
||||
|
||||
tracing::info!("MCP Memory Server running on http://127.0.0.1:3000/ws");
|
||||
let addr = std::env::var("MCP_PORT").unwrap_or_else(|_| "3000".to_string());
|
||||
let addr: std::net::SocketAddr = format!("127.0.0.1:{}", addr)
|
||||
let port_str = std::env::var("MCP_PORT").unwrap_or_else(|_| "3000".to_string());
|
||||
tracing::info!("MCP Memory Server running on http://127.0.0.1:{}/ws", port_str);
|
||||
let addr: std::net::SocketAddr = format!("127.0.0.1:{}", port_str)
|
||||
.parse()
|
||||
.expect("Invalid bind address");
|
||||
|
||||
@@ -282,7 +293,12 @@ async fn run_server(state: Arc<MemoryState>) -> Result<(), Box<dyn std::error::E
|
||||
return Ok(());
|
||||
}
|
||||
};
|
||||
if let Err(e) = axum::serve(listener, app.into_make_service()).await {
|
||||
if let Err(e) = axum::serve(listener, app.into_make_service())
|
||||
.with_graceful_shutdown(async move {
|
||||
let _ = shutdown_rx.await;
|
||||
})
|
||||
.await
|
||||
{
|
||||
let log_path = dirs::home_dir()
|
||||
.unwrap_or_default()
|
||||
.join(".gemini/mcp_memory/daemon_error.log");
|
||||
|
||||
@@ -383,6 +383,13 @@ impl MemoryHandler {
|
||||
register!(workspaces::AddPrChecklistItemHandler);
|
||||
register!(workspaces::GetPrChecklistHandler);
|
||||
register!(workspaces::ClearPrChecklistHandler);
|
||||
register!(vision::ReadClipboardHandler);
|
||||
register!(vision::WriteClipboardHandler);
|
||||
register!(git::GetActiveWorktreeContextHandler);
|
||||
register!(logs::WatchProcessLogsHandler);
|
||||
register!(logs::GetRecentLogsHandler);
|
||||
register!(ast::ReadFileSkeletonHandler);
|
||||
register!(vision::ToggleClipboardWatchModeHandler);
|
||||
|
||||
Self {
|
||||
state,
|
||||
|
||||
@@ -14,6 +14,7 @@ pub struct GenericEvent {
|
||||
|
||||
pub struct MemoryState {
|
||||
pub base_dir: PathBuf,
|
||||
pub clipboard_watch_mode: tokio::sync::RwLock<bool>,
|
||||
pub graph: Store<KnowledgeGraph>,
|
||||
pub search_index: RwLock<MemoryIndex>,
|
||||
pub ledger: Store<Vec<CodeChange>>,
|
||||
@@ -48,6 +49,7 @@ impl MemoryState {
|
||||
let db = crate::db::init_redb(&base);
|
||||
|
||||
Self {
|
||||
clipboard_watch_mode: tokio::sync::RwLock::new(false),
|
||||
graph: Store::new("knowledge_graph_master", db.clone()),
|
||||
base_dir: base.clone(),
|
||||
search_index: RwLock::new(match crate::search::MemoryIndex::new(&base) {
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
use arboard::{Clipboard, ImageData};
|
||||
use image::GenericImageView;
|
||||
use std::borrow::Cow;
|
||||
|
||||
fn main() {
|
||||
let img = image::open("C:/Users/reazul.ashraf/workspace/rust/mcp-memory/test.jpg").unwrap_or_else(|_| image::DynamicImage::new_rgb8(10, 10));
|
||||
let rgba = img.into_rgba8();
|
||||
let (w, h) = rgba.dimensions();
|
||||
|
||||
let img_data = ImageData {
|
||||
width: w as usize,
|
||||
height: h as usize,
|
||||
bytes: Cow::Owned(rgba.into_raw()),
|
||||
};
|
||||
|
||||
let mut clipboard = Clipboard::new().unwrap();
|
||||
clipboard.set_image(img_data).unwrap();
|
||||
println!("Successfully wrote image");
|
||||
}
|
||||
@@ -1,6 +1,23 @@
|
||||
use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// Extracts the current contents of the OS clipboard (text, HTML, files, and image).
|
||||
#[derive(Debug, Deserialize, Serialize, JsonSchema)]
|
||||
pub struct ReadClipboardTool {}
|
||||
|
||||
/// Writes contents to the OS clipboard. You can provide any combination of text, HTML, file paths, or an image path.
|
||||
#[derive(Debug, Deserialize, Serialize, JsonSchema)]
|
||||
pub struct WriteClipboardTool {
|
||||
/// Optional plain text to write to the clipboard.
|
||||
pub text: Option<String>,
|
||||
/// Optional HTML to write to the clipboard (useful for rich text pasting).
|
||||
pub html: Option<String>,
|
||||
/// Optional absolute file paths to write to the clipboard as a Windows File Drop.
|
||||
pub files: Option<Vec<String>>,
|
||||
/// Optional absolute path to a local image to write to the clipboard.
|
||||
pub image_path: Option<String>,
|
||||
}
|
||||
|
||||
/// Create new entities in the knowledge graph.
|
||||
#[derive(Debug, Deserialize, Serialize, JsonSchema)]
|
||||
pub struct CreateEntitiesTool {
|
||||
@@ -637,3 +654,28 @@ mod tests {
|
||||
);
|
||||
}
|
||||
}
|
||||
/// Get the active worktree context, including branch name, modified files, and a truncated git diff.
|
||||
#[derive(Debug, Deserialize, Serialize, JsonSchema)]
|
||||
pub struct GetActiveWorktreeContextTool {}
|
||||
/// Tail a specific log file in the background so it can be queried later.
|
||||
#[derive(Debug, Deserialize, Serialize, JsonSchema)]
|
||||
pub struct WatchProcessLogsTool {
|
||||
pub file_path: String,
|
||||
}
|
||||
|
||||
/// Get the recent logs from a watched file.
|
||||
#[derive(Debug, Deserialize, Serialize, JsonSchema)]
|
||||
pub struct GetRecentLogsTool {
|
||||
pub file_path: String,
|
||||
}
|
||||
|
||||
/// Enable or disable clipboard watch mode (auto-ingesting into StickyNotes).
|
||||
#[derive(Debug, Deserialize, Serialize, JsonSchema)]
|
||||
pub struct ToggleClipboardWatchModeTool {
|
||||
pub enable: bool,
|
||||
}
|
||||
/// Read a file and return only its AST skeleton (Imports, Structs, Enums, Traits, Functions)
|
||||
#[derive(Debug, Deserialize, Serialize, JsonSchema)]
|
||||
pub struct ReadFileSkeletonTool {
|
||||
pub file_path: String,
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
param (
|
||||
[string]$Port = "3000"
|
||||
)
|
||||
|
||||
$cmdPath = "$env:USERPROFILE\.local\bin\start_mcp.cmd"
|
||||
|
||||
$startupClass = [wmiclass]"Win32_ProcessStartup"
|
||||
$startup = $startupClass.CreateInstance()
|
||||
$startup.ShowWindow = 0 # SW_HIDE
|
||||
|
||||
$processClass = [wmiclass]"Win32_Process"
|
||||
$processClass.Create("cmd /c `"$cmdPath`" $Port", $null, $startup) | Out-Null
|
||||
+18
-10
@@ -1,24 +1,32 @@
|
||||
$Url = "http://127.0.0.1:3000/"
|
||||
param (
|
||||
[string]$Port = "3000"
|
||||
)
|
||||
$HealthUrl = "http://127.0.0.1:$Port/ping"
|
||||
$UiUrl = "http://127.0.0.1:$Port/?t=$(Get-Random)"
|
||||
$ExpectedContent = "open-color" # Basic sanity check that we are serving the updated UI
|
||||
|
||||
Write-Host "Verifying server at $Url..." -ForegroundColor Cyan
|
||||
Write-Host "Verifying server HEALTH at $HealthUrl..." -ForegroundColor Cyan
|
||||
|
||||
for ($i = 1; $i -le 10; $i++) {
|
||||
for ($i = 1; $i -le 40; $i++) {
|
||||
try {
|
||||
$response = Invoke-WebRequest -Uri $Url -UseBasicParsing -ErrorAction Stop
|
||||
if ($response.StatusCode -eq 200) {
|
||||
if ($response.Content -match $ExpectedContent) {
|
||||
Write-Host "✅ Server is UP and serving the correct payload! (Attempt $i)" -ForegroundColor Green
|
||||
$healthResponse = Invoke-WebRequest -Uri $HealthUrl -UseBasicParsing -ErrorAction Stop
|
||||
if ($healthResponse.StatusCode -eq 200) {
|
||||
Write-Host "✅ Server /ping is UP! (Attempt $i)" -ForegroundColor Green
|
||||
|
||||
Write-Host "Verifying UI payload at $UiUrl..." -ForegroundColor Cyan
|
||||
$uiResponse = Invoke-WebRequest -Uri $UiUrl -UseBasicParsing -ErrorAction Stop
|
||||
if ($uiResponse.Content -match $ExpectedContent) {
|
||||
Write-Host "✅ Server is serving the correct UI payload!" -ForegroundColor Green
|
||||
exit 0
|
||||
} else {
|
||||
Write-Host "⚠️ Server is up but serving old cached content. Retrying... (Attempt $i)" -ForegroundColor Yellow
|
||||
Write-Host "⚠️ Server is healthy but UI serving old cached content. Retrying..." -ForegroundColor Yellow
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
Write-Host "⏳ Server not responding yet... (Attempt $i)" -ForegroundColor Yellow
|
||||
Write-Host "⏳ Server /ping not responding yet... (Attempt $i)" -ForegroundColor Yellow
|
||||
}
|
||||
Start-Sleep -Seconds 1
|
||||
}
|
||||
|
||||
Write-Error "❌ Deployment verification failed! Server did not come up or did not serve correct content after 10 seconds."
|
||||
Write-Error "❌ Deployment verification failed! /ping endpoint unreachable after 40 seconds."
|
||||
exit 1
|
||||
Reference in new issue
Block a user