Skip to main content
xpcoffee

nvim-markdown-notes-memgraph

by xpcoffeev0.1.0

Memgraph-powered knowledge graph MCP server for markdown notes

Installation guide →
1 skill GitHub

Documentation

# nvim-markdown-notes-memgraph

Standalone CLI for Memgraph + MCP server with Docker Compose orchestration.

Extracts the Memgraph graph database functionality from [nvim-markdown-notes](https://github.com/username/nvim-markdown-notes) into a reusable, independently installable package.

## Features

- Docker Compose orchestration for Memgraph and MCP server
- CLI commands for managing services (start, stop, status)
- MCP server for AI assistant integration
- Neovim bridge for editor integration
- Automatic entity extraction (wikilinks, mentions, hashtags)

## Requirements

- Python 3.10+
- Docker
- Docker Compose
- **Build tools** (needed to compile the Memgraph client):
  - Ubuntu/Debian: `sudo apt install cmake build-essential libssl-dev`
  - macOS: `brew install cmake openssl`
  - Arch: `sudo pacman -S cmake base-devel openssl`

Docker and Docker Compose must be installed and running before using this package. The CLI manages Docker Compose services, so these are hard prerequisites.

To verify Docker is installed:

```bash
docker --version
docker compose version
```

## Installation

### Using pip

```bash
pip install nvim-markdown-notes-memgraph
```

### Using uv

```bash
uv pip install nvim-markdown-notes-memgraph
```

### From source

```bash
git clone https://github.com/username/nvim-markdown-notes-memgraph.git
cd nvim-markdown-notes-memgraph
pip install -e .
```

### Auto-install from Neovim

If you use the [nvim-markdown-notes](https://github.com/username/nvim-markdown-notes) plugin with `memgraph.enabled = true`, the plugin will detect the missing CLI and offer to install it for you automatically. See [Auto-install from Neovim](#auto-install-from-neovim-1) for details.

### Verify installation

```bash
nvim-markdown-notes-memgraph --help
```

### Updating

To update to the latest version:

```bash
uv tool install --force git+https://github.com/xpcoffee/nvim-markdown-notes-memgraph.git
```

Or if installed via pip:

```bash
pip install --upgrade git+https://github.com/xpcoffee/nvim-markdown-notes-memgraph.git
```

## Usage

The CLI provides several commands for managing the Memgraph database and MCP server.

### Global Options

All commands support the following global option:

- `--notes-root PATH`: Root directory for markdown notes (defaults to `~/notes` or `$NOTES_ROOT` environment variable)

Example:

```bash
nvim-markdown-notes-memgraph --notes-root /path/to/notes start
```

Or set the environment variable:

```bash
export NOTES_ROOT=/path/to/notes
nvim-markdown-notes-memgraph start
```

### Commands

#### start

Start Docker Compose services (Memgraph + MCP server).

```bash
nvim-markdown-notes-memgraph start
```

With custom notes directory:

```bash
nvim-markdown-notes-memgraph --notes-root ~/Documents/notes start
```

This command:

- Starts the Memgraph database container
- Starts the MCP server container
- Waits for services to be healthy (up to 60 seconds)
- Mounts your notes directory read-only into the MCP server container

Output:

```
Starting services with notes root: /home/user/notes
Services are healthy and ready!
Memgraph is running on localhost:7687
MCP server is connected and monitoring: /home/user/notes
```

#### stop

Stop Docker Compose services (Memgraph + MCP server).

```bash
nvim-markdown-notes-memgraph stop
```

This command stops all running containers but preserves the Memgraph data volume.

Output:

```
Stopping services...
Services stopped successfully.
```

#### status

Show status of Docker Compose services (Memgraph + MCP server).

```bash
nvim-markdown-notes-memgraph status
```

Output:

```
Service Status:
--------------------------------------------------------------------------------
  Service: memgraph
  Name:    nvim-markdown-notes-memgraph-memgraph-1
  State:   running
  Health:  healthy
  Ports:   7687:7687
--------------------------------------------------------------------------------
  Service: mcp-server
  Name:    nvim-markdown-notes-memgraph-mcp-server-1
  State:   running
  Health:  N/A
  Ports:   N/A
--------------------------------------------------------------------------------
```

If no services are running:

```
No services are running.
Run 'nvim-markdown-notes-memgraph start' to start services.
```

#### config

Output MCP JSON configuration for use with MCP clients.

```bash
nvim-markdown-notes-memgraph config
```

With custom Memgraph host/port:

```bash
nvim-markdown-notes-memgraph config --memgraph-host localhost --memgraph-port 7687
```

Output (example):

```json
{
  "mcpServers": {
    "nvim-markdown-notes-memgraph": {
      "command": "nvim-markdown-notes-memgraph",
      "args": ["serve"],
      "env": {
        "MEMGRAPH_HOST": "localhost",
        "MEMGRAPH_PORT": "7687",
        "NOTES_ROOT": "/home/user/notes"
      }
    }
  }
}
```

This configuration can be added to MCP client config files (e.g., Claude Desktop, Continue, etc.).

#### serve

Run the MCP server directly (for container use or direct MCP client connections).

```bash
nvim-markdown-notes-memgraph serve
```

With custom configuration:

```bash
nvim-markdown-notes-memgraph --notes-root ~/notes serve --memgraph-host localhost --memgraph-port 7687
```

Or using environment variables:

```bash
export MEMGRAPH_HOST=localhost
export MEMGRAPH_PORT=7687
export NOTES_ROOT=~/notes
nvim-markdown-notes-memgraph serve
```

This command starts the MCP server over stdio. It's intended for use as a Docker container entrypoint or for direct MCP client connections.

#### bridge

Run the Neovim bridge (stdin/stdout JSON).

```bash
nvim-markdown-notes-memgraph bridge
```

With custom configuration:

```bash
nvim-markdown-notes-memgraph --notes-root ~/notes bridge --memgraph-host localhost --memgraph-port 7687
```

Or using environment variables:

```bash
export MEMGRAPH_HOST=localhost
export MEMGRAPH_PORT=7687
export NOTES_ROOT=~/notes
nvim-markdown-notes-memgraph bridge
```

This command starts the Memgraph bridge for Neovim integration. It communicates via JSON over stdin/stdout and uses the Bolt protocol to connect to Memgraph.

The bridge supports actions like:

- `connect`: Establish connection to Memgraph
- `health_check`: Check if connection is alive
- `update_note`: Update a note and its relationships in the graph
- `delete_note`: Remove a note from the graph
- `query`: Execute a Cypher query
- `reindex`: Rebuild the entire graph from scratch
- `stats`: Get graph statistics

## Docker Configuration

### Services

The Docker Compose setup includes two services:

1. **memgraph**: The Memgraph graph database (port 7687)
2. **mcp-server**: The MCP server that connects to Memgraph and monitors your notes

### Volumes

The Docker Compose setup uses two types of volumes for data persistence:

#### Memgraph Data Volume (Named Volume)

A named volume `memgraph-data` is automatically created to persist the Memgraph database across container restarts. This ensures your graph data is not lost when stopping services.

The volume is mounted at `/var/lib/memgraph` inside the Memgraph container.

#### Notes Directory (Bind Mount)

Your markdown notes directory is mounted read-only into the MCP server container at `/notes`.

**Default location**: `~/notes`

**Custom location**: Use the `--notes-root` option or set the `NOTES_ROOT` environment variable:

```bash
# Using environment variable
export NOTES_ROOT=/path/to/my/notes
nvim-markdown-notes-memgraph start

# Or with CLI option
nvim-markdown-notes-memgraph --notes-root /path/to/my/notes start
```

The bind mount is read-only (`:ro`) to prevent accidental modifications to your notes from within the container.

### Managing Volumes

```bash
# List volumes
docker volume ls

# Inspect the Memgraph data volume
docker volume inspect nvim-markdown-notes-memgraph_memgraph-data

# Remove volume (WARNING: deletes all graph data)
docker compose down -v
```

## Graph Schema

The system extracts entities from your markdown notes and stores them in Memgraph with the following schema:

### Nodes

- `(:Note {path, title, filename, last_modified, content_hash})` - Markdown notes
- `(:Person {name, display_name})` - People mentioned with `@username`
- `(:Tag {name})` - Hashtags like `#project`

### Relationships

- `(:Note)-[:LINKS_TO {line_number}]->(:Note)` - Wikilinks between notes
- `(:Note)-[:MENTIONS {line_number}]->(:Person)` - Mentions of people
- `(:Note)-[:HAS_TAG {line_number}]->(:Tag)` - Tags in notes
- `(:Person)-[:HAS_NOTE]->(:Note)` - Person nodes link to notes about them

### Entity Extraction

The system uses regex patterns to extract entities:

- **Wikilinks**: `[[note-name]]` or `[[note-name|display text]]`
- **Mentions**: `@username` (alphanumeric with hyphens/underscores)
- **Hashtags**: `#tagname` (alphanumeric with hyphens/underscores)

## MCP Integration

The MCP (Model Context Protocol) server provides tools for AI assistants to query and interact with your note graph.

### Available MCP Tools

The server exposes several tools that AI assistants can call:

- **find_notes_by_tag**: Find all notes containing a specific tag
- **find_notes_mentioning_person**: Find notes that mention a person
- **find_linked_notes**: Find notes linked to/from a specific note
- **search_notes_content**: Full-text search across note content
- **get_note_stats**: Get statistics about a note (links, mentions, tags)
- **list_all_tags**: List all tags used across all notes
- **list_all_people**: List all people mentioned in notes

### Using with Claude Desktop

Add the configuration to `~/Library/Application Support/Claude/claude_desktop_config.json` (macOS) or `%APPDATA%\Claude\claude_desktop_config.json` (Windows):

```bash
# Generate the config
nvim-markdown-notes-memgraph config >> claude_desktop_config.json
```

Then restart Claude Desktop.

### Using with Continue

Add to your Continue configuration file:

```bash
nvim-markdown-notes-memgraph config
```

Copy the output to your Continue MCP servers configuration.

## Neovim Integration

The CLI provides a `bridge` command that enables Neovim plugins to communicate with Memgraph via a JSON-over-stdin/stdout protocol.

### Auto-install from Neovim

When `memgraph.enabled = true` in your plugin config and the CLI is not on PATH, the plugin will prompt you to install it. The install uses whichever tool is available: `uv tool install` (preferred), `pip3 install`, or `pip install`.

**Configuration options** (passed via `opts.memgraph`):

| Option | Default | Description |
|--------|---------|-------------|
| `install_prompt` | `true` | Set to `false` to never prompt for installation |
| `install_source` | `"git+https://github.com/xpcoffee/nvim-markdown-notes-memgraph.git"` | Package specifier passed to pip/uv. Override with a PyPI name or local path. |

Example configuration:

```lua
require("nvim-markdown-notes").setup({
  notes_root_path = "~/notes",
  memgraph = {
    enabled = true,
    -- Disable auto-install prompt
    install_prompt = false,
    -- Or point to a local checkout
    install_source = "/path/to/nvim-markdown-notes-memgraph",
  },
})
```

**Behaviour:**

- If the CLI is already on PATH, no prompt is shown.
- If the user declines the prompt, it won't appear again until Neovim restarts.
- If `python3` is not available, or neither `uv` nor `pip` is found, the prompt is silently skipped and the plugin falls back to bundled scripts.
- Run `:MemgraphInstallCLI` at any time to trigger the install prompt manually (resets any prior decline).

### Bridge Protocol

The bridge command runs a persistent process that reads JSON requests from stdin and writes JSON responses to stdout. Each request/response is a single line of JSON.

**Request format:**

```json
{
  "action": "action_name",
  "params": {
    "key": "value"
  }
}
```

**Response format:**

```json
{
  "success": true,
  "data": {},
  "error": null
}
```

### Supported Actions

The bridge supports the following actions:

- **connect**: Establish connection to Memgraph

  ```json
  { "action": "connect", "params": { "host": "localhost", "port": 7687 } }
  ```

- **health_check**: Check if connection is alive

  ```json
  { "action": "health_check", "params": {} }
  ```

- **update_note**: Update a note and its relationships in the graph

  ```json
  {
    "action": "update_note",
    "params": {
      "path": "/path/to/note.md",
      "title": "Note Title",
      "content": "Note content...",
      "wikilinks": [{ "target_path": "/path/to/other.md", "line_number": 5 }],
      "mentions": [{ "name": "username", "line_number": 10 }],
      "hashtags": [{ "name": "tagname", "line_number": 15 }]
    }
  }
  ```

- **delete_note**: Remove a note from the graph

  ```json
  { "action": "delete_note", "params": { "path": "/path/to/note.md" } }
  ```

- **query**: Execute a Cypher query

  ```json
  {
    "action": "query",
    "params": {
      "cypher": "MATCH (n:Note) RETURN n.title LIMIT 5",
      "params": {}
    }
  }
  ```

- **reindex**: Rebuild the entire graph from scratch

  ```json
  {
    "action": "reindex",
    "params": {
      "notes": [
        {
          "path": "/path/to/note.md",
          "title": "Note Title",
          "content": "Content...",
          "wikilinks": [],
          "mentions": [],
          "hashtags": []
        }
      ]
    }
  }
  ```

- **stats**: Get graph statistics

  ```json
  { "action": "stats", "params": {} }
  ```

- **quit**: Gracefully shut down the bridge
  ```json
  { "action": "quit", "params": {} }
  ```

### Example: Neovim Lua Integration

Here's an example of how to integrate the CLI bridge into a Neovim plugin:

```lua
local M = {}

-- Job ID for the bridge process
local job_id = nil
local is_connected = false

-- Callback management
local pending_callbacks = {}
local callback_counter = 0
local response_buffer = ""

-- Parse JSON response
local function parse_response(line)
  local ok, result = pcall(vim.json.decode, line)
  if ok then
    return result
  end
  return nil
end

-- Send a request to the bridge
local function send_request(action, params, callback)
  if not job_id then
    if callback then
      callback(false, nil, "Bridge not started")
    end
    return
  end

  local request = vim.json.encode({
    action = action,
    params = params or {}
  })

  if callback then
    callback_counter = callback_counter + 1
    pending_callbacks[callback_counter] = callback
  end

  vim.fn.chansend(job_id, request .. "\n")
end

-- Handle stdout from bridge
local function on_stdout(_, data, _)
  for _, line in ipairs(data) do
    if line and line ~= "" then
      response_buffer = response_buffer .. line

      local response = parse_response(response_buffer)
      if response then
        response_buffer = ""

        -- Find oldest pending callback
        local oldest_key = nil
        for key, _ in pairs(pending_callbacks) do
          if oldest_key == nil or key < oldest_key then
            oldest_key = key
          end
        end

        if oldest_key and pending_callbacks[oldest_key] then
          local cb = pending_callbacks[oldest_key]
          pending_callbacks[oldest_key] = nil
          cb(response.success, response.data, response.error)
        end

        -- Update connection state
        if type(response.data) == "table" then
          if response.data.status == "healthy" then
            is_connected = true
          elseif response.data.message and response.data.message:match("^Connected") then
            is_connected = true
          end
        end
      end
    end
  end
end

-- Start the bridge process
function M.start_bridge(callback)
  if job_id then
    if callback then
      callback(true, "Bridge already running")
    end
    return
  end

  -- Start the bridge command
  local cmd = { "nvim-markdown-notes-memgraph", "bridge" }

  job_id = vim.fn.jobstart(cmd, {
    on_stdout = on_stdout,
    on_exit = function(_, exit_code, _)
      job_id = nil
      is_connected = false
      for key, cb in pairs(pending_callbacks) do
        cb(false, nil, "Bridge exited with code " .. exit_code)
        pending_callbacks[key] = nil
      end
    end,
    stdout_buffered = false,
  })

  if job_id <= 0 then
    job_id = nil
    if callback then
      callback(false, "Failed to start bridge")
    end
    return
  end

  -- Connect to Memgraph
  vim.defer_fn(function()
    send_request("connect", { host = "localhost", port = 7687 }, callback)
  end, 100)
end

-- Ensure services are running and connected
function M.ensure_services(callback)
  -- Check if CLI is installed
  if vim.fn.executable("nvim-markdown-notes-memgraph") == 0 then
    if callback then
      callback(false, nil, "nvim-markdown-notes-memgraph CLI not installed")
    end
    return
  end

  -- Start services if not running
  vim.fn.jobstart(
    { "nvim-markdown-notes-memgraph", "start" },
    {
      on_exit = function(_, exit_code, _)
        if exit_code == 0 then
          -- Services started, now start bridge
          M.start_bridge(callback)
        else
          if callback then
            callback(false, nil, "Failed to start services")
          end
        end
      end
    }
  )
end

-- Update a note in the graph
function M.update_note(path, title, content, entities, callback)
  send_request("update_note", {
    path = path,
    title = title,
    content = content,
    wikilinks = entities.wikilinks or {},
    mentions = entities.mentions or {},
    hashtags = entities.hashtags or {}
  }, callback)
end

-- Get graph statistics
function M.get_stats(callback)
  send_request("stats", {}, callback)
end

return M
```

### Usage in Your Plugin

```lua
local graph = require("your_plugin.graph")

-- Ensure services are running and connect
graph.ensure_services(function(success, data, err)
  if success then
    print("Connected to Memgraph!")

    -- Update a note
    graph.update_note(
      "/home/user/notes/example.md",
      "Example Note",
      "This is the content...",
      {
        wikilinks = {
          { target_path = "/home/user/notes/other.md", line_number = 5 }
        },
        mentions = {
          { name = "alice", line_number = 10 }
        },
        hashtags = {
          { name = "project", line_number = 15 }
        }
      },
      function(success, data, err)
        if success then
          print("Note updated!")
        else
          print("Error: " .. (err or "unknown"))
        end
      end
    )
  else
    print("Failed to connect: " .. (err or "unknown"))
  end
end)
```

### ensure_services() Pattern

The `ensure_services()` pattern is the recommended way to integrate the CLI with your Neovim plugin:

1. **Check if CLI is installed**: Verify that `nvim-markdown-notes-memgraph` is in PATH
2. **Start services**: Run `nvim-markdown-notes-memgraph start` to ensure Memgraph and MCP server are running
3. **Start bridge**: Launch the bridge command and connect to Memgraph
4. **Handle errors gracefully**: Provide helpful error messages if any step fails

This pattern ensures that:

- Services are automatically started when needed
- Users don't need to manually start Docker containers
- The plugin works out of the box after installing the CLI
- Service management is transparent to the user

### Environment Variables

The bridge command respects the following environment variables:

- `NOTES_ROOT`: Root directory for markdown notes (default: `~/notes`)
- `MEMGRAPH_HOST`: Memgraph host (default: `localhost`)
- `MEMGRAPH_PORT`: Memgraph port (default: `7687`)

You can set these in your Neovim configuration:

```lua
vim.env.NOTES_ROOT = vim.fn.expand("~/Documents/notes")
vim.env.MEMGRAPH_HOST = "localhost"
vim.env.MEMGRAPH_PORT = "7687"
```

Or pass them when starting the bridge:

```bash
NOTES_ROOT=~/Documents/notes nvim-markdown-notes-memgraph bridge
```

## Troubleshooting

### Services won't start

**Problem**: `docker compose` command fails or services won't start.

**Solutions**:

1. Check that Docker is running:

   ```bash
   docker ps
   ```

2. Check that Docker Compose is installed:

   ```bash
   docker compose version
   ```

3. Check for port conflicts (Memgraph uses port 7687):

   ```bash
   lsof -i :7687
   ```

4. Check Docker logs:

   ```bash
   docker compose logs memgraph
   docker compose logs mcp-server
   ```

5. Try stopping and removing all containers:

   ```bash
   docker compose down
   nvim-markdown-notes-memgraph start
   ```

### Services are unhealthy

**Problem**: `start` command times out waiting for services to be healthy.

**Solutions**:

1. Check service logs:

   ```bash
   docker compose logs memgraph
   ```

2. Wait longer - the healthcheck can take up to 60 seconds.

3. Restart the Memgraph service:

   ```bash
   docker compose restart memgraph
   ```

### Notes directory not found

**Problem**: Error about notes directory not existing.

**Solutions**:

1. Verify the path exists:

   ```bash
   ls ~/notes
   # or
   ls /path/to/notes
   ```

2. Create the directory:

   ```bash
   mkdir -p ~/notes
   ```

3. Set the correct path:

   ```bash
   nvim-markdown-notes-memgraph --notes-root /correct/path start
   ```

### CLI not installed

**Problem**: The plugin falls back to bundled scripts or reports the CLI is missing.

**Solutions**:

1. Run `:MemgraphInstallCLI` inside Neovim to trigger the auto-install prompt.

2. Or install manually:

   ```bash
   pip install git+https://github.com/xpcoffee/nvim-markdown-notes-memgraph.git
   ```

3. Verify the CLI is on PATH:

   ```bash
   nvim-markdown-notes-memgraph --help
   ```

### Installation fails with cmake error

**Problem**: `pip install` or `uv tool install` fails with a long error trace mentioning cmake or CMakeLists.txt.

**Solution**: The `pymgclient` dependency requires cmake and a C compiler to build from source. Install the build prerequisites:

```
Ubuntu/Debian: sudo apt install cmake build-essential libssl-dev
macOS:         brew install cmake openssl
```

Then retry the installation.

### Connection refused to Memgraph

**Problem**: Cannot connect to Memgraph on `localhost:7687`.

**Solutions**:

1. Check that Memgraph is running:

   ```bash
   nvim-markdown-notes-memgraph status
   ```

2. Check that the port is exposed:

   ```bash
   docker compose ps
   ```

3. Try connecting directly:

   ```bash
   docker exec -it nvim-markdown-notes-memgraph-memgraph-1 mgconsole
   ```

### MCP server not responding

**Problem**: MCP server doesn't respond to requests.

**Solutions**:

1. Check server logs:

   ```bash
   docker compose logs mcp-server
   ```

2. Restart the MCP server:

   ```bash
   docker compose restart mcp-server
   ```

3. Verify environment variables are set correctly:

   ```bash
   docker compose config
   ```

### Graph data lost after restart

**Problem**: Graph data disappears after stopping services.

**Solutions**:

1. Make sure you're using `stop` instead of `down`:

   ```bash
   nvim-markdown-notes-memgraph stop  # Preserves data
   ```

2. Avoid using `docker compose down -v` which deletes volumes.

3. Check that the volume exists:

   ```bash
   docker volume ls | grep memgraph-data
   ```

### Permission errors in Docker

**Problem**: Permission denied errors when mounting notes directory.

**Solutions**:

1. Check file permissions on your notes directory:

   ```bash
   ls -la ~/notes
   ```

2. Make sure Docker has permission to access the directory (especially on macOS - check Docker Desktop settings).

3. On Linux, check SELinux/AppArmor settings if applicable.

## Development

### Running tests

```bash
# Install dev dependencies
pip install -e ".[dev]"

# Run tests
pytest

# Run tests with coverage
pytest --cov=src/nvim_markdown_notes_memgraph
```

### Building from source

```bash
# Install in development mode
pip install -e .

# Build wheel
pip install build
python -m build
```

## License

MIT