notes-vault-mcp 0.2.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- notes_vault_mcp-0.2.0/.dockerignore +8 -0
- notes_vault_mcp-0.2.0/.github/workflows/ci.yml +22 -0
- notes_vault_mcp-0.2.0/.github/workflows/release.yml +83 -0
- notes_vault_mcp-0.2.0/.gitignore +11 -0
- notes_vault_mcp-0.2.0/Dockerfile +13 -0
- notes_vault_mcp-0.2.0/LICENSE +21 -0
- notes_vault_mcp-0.2.0/PKG-INFO +262 -0
- notes_vault_mcp-0.2.0/README.md +243 -0
- notes_vault_mcp-0.2.0/notes_vault_mcp/__init__.py +3 -0
- notes_vault_mcp-0.2.0/notes_vault_mcp/backends/__init__.py +70 -0
- notes_vault_mcp-0.2.0/notes_vault_mcp/backends/local.py +79 -0
- notes_vault_mcp-0.2.0/notes_vault_mcp/backends/s3.py +106 -0
- notes_vault_mcp-0.2.0/notes_vault_mcp/changelog.py +54 -0
- notes_vault_mcp-0.2.0/notes_vault_mcp/cli.py +162 -0
- notes_vault_mcp-0.2.0/notes_vault_mcp/config.py +58 -0
- notes_vault_mcp-0.2.0/notes_vault_mcp/frontmatter.py +121 -0
- notes_vault_mcp-0.2.0/notes_vault_mcp/hooks.py +150 -0
- notes_vault_mcp-0.2.0/notes_vault_mcp/index.py +268 -0
- notes_vault_mcp-0.2.0/notes_vault_mcp/notes.py +420 -0
- notes_vault_mcp-0.2.0/notes_vault_mcp/schema.py +239 -0
- notes_vault_mcp-0.2.0/notes_vault_mcp/search.py +235 -0
- notes_vault_mcp-0.2.0/notes_vault_mcp/server.py +201 -0
- notes_vault_mcp-0.2.0/notes_vault_mcp/templates/Areas.base +19 -0
- notes_vault_mcp-0.2.0/notes_vault_mcp/templates/Open tasks.base +26 -0
- notes_vault_mcp-0.2.0/notes_vault_mcp/templates/Resources.base +22 -0
- notes_vault_mcp-0.2.0/notes_vault_mcp/templates/__init__.py +0 -0
- notes_vault_mcp-0.2.0/notes_vault_mcp/templates/claude-md-snippet.md +15 -0
- notes_vault_mcp-0.2.0/notes_vault_mcp/templates/schema.yml +89 -0
- notes_vault_mcp-0.2.0/notes_vault_mcp/vault.py +26 -0
- notes_vault_mcp-0.2.0/pyproject.toml +50 -0
- notes_vault_mcp-0.2.0/server.json +103 -0
- notes_vault_mcp-0.2.0/tests/conftest.py +46 -0
- notes_vault_mcp-0.2.0/tests/fixtures/real-queries.json +567 -0
- notes_vault_mcp-0.2.0/tests/fixtures/vault/.obsidian/app.json +1 -0
- notes_vault_mcp-0.2.0/tests/fixtures/vault/.vault/schema.yml +5 -0
- notes_vault_mcp-0.2.0/tests/fixtures/vault/Archive/old-plan.md +11 -0
- notes_vault_mcp-0.2.0/tests/fixtures/vault/Archive/wrong-status.md +11 -0
- notes_vault_mcp-0.2.0/tests/fixtures/vault/Areas/greenhouse.md +15 -0
- notes_vault_mcp-0.2.0/tests/fixtures/vault/Areas/homelab.md +13 -0
- notes_vault_mcp-0.2.0/tests/fixtures/vault/Log/greenhouse-log.md +12 -0
- notes_vault_mcp-0.2.0/tests/fixtures/vault/Projects/greenhouse-draft.md +13 -0
- notes_vault_mcp-0.2.0/tests/fixtures/vault/Projects/greenhouse-fresh.md +13 -0
- notes_vault_mcp-0.2.0/tests/fixtures/vault/Projects/homelab-stale.md +12 -0
- notes_vault_mcp-0.2.0/tests/fixtures/vault/Resources/booking-trap.md +13 -0
- notes_vault_mcp-0.2.0/tests/fixtures/vault/Resources/broken-yaml.md +7 -0
- notes_vault_mcp-0.2.0/tests/fixtures/vault/Resources/homelab.md +11 -0
- notes_vault_mcp-0.2.0/tests/fixtures/vault/Resources/no-frontmatter.md +1 -0
- notes_vault_mcp-0.2.0/tests/fixtures/vault/Resources/old-domain-howto.md +12 -0
- notes_vault_mcp-0.2.0/tests/test_backends.py +109 -0
- notes_vault_mcp-0.2.0/tests/test_cli_hooks.py +214 -0
- notes_vault_mcp-0.2.0/tests/test_frontmatter.py +113 -0
- notes_vault_mcp-0.2.0/tests/test_index.py +95 -0
- notes_vault_mcp-0.2.0/tests/test_lint.py +78 -0
- notes_vault_mcp-0.2.0/tests/test_notes.py +215 -0
- notes_vault_mcp-0.2.0/tests/test_real_queries.py +32 -0
- notes_vault_mcp-0.2.0/tests/test_search.py +107 -0
- notes_vault_mcp-0.2.0/tests/test_server.py +127 -0
- notes_vault_mcp-0.2.0/uv.lock +1262 -0
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main]
|
|
6
|
+
pull_request:
|
|
7
|
+
|
|
8
|
+
jobs:
|
|
9
|
+
test:
|
|
10
|
+
runs-on: ubuntu-latest
|
|
11
|
+
strategy:
|
|
12
|
+
fail-fast: false
|
|
13
|
+
matrix:
|
|
14
|
+
python-version: ["3.11", "3.12", "3.13"]
|
|
15
|
+
steps:
|
|
16
|
+
- uses: actions/checkout@v5
|
|
17
|
+
- uses: astral-sh/setup-uv@v6
|
|
18
|
+
with:
|
|
19
|
+
enable-cache: true
|
|
20
|
+
- run: uv sync --frozen --python ${{ matrix.python-version }}
|
|
21
|
+
- run: uv run ruff check .
|
|
22
|
+
- run: uv run pytest -q
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
name: Release
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
tags: ["v*"]
|
|
6
|
+
|
|
7
|
+
jobs:
|
|
8
|
+
test:
|
|
9
|
+
runs-on: ubuntu-latest
|
|
10
|
+
steps:
|
|
11
|
+
- uses: actions/checkout@v5
|
|
12
|
+
- uses: astral-sh/setup-uv@v6
|
|
13
|
+
- run: uv sync --frozen
|
|
14
|
+
- run: uv run ruff check .
|
|
15
|
+
- run: uv run pytest -q
|
|
16
|
+
|
|
17
|
+
pypi:
|
|
18
|
+
needs: [test]
|
|
19
|
+
runs-on: ubuntu-latest
|
|
20
|
+
environment: pypi
|
|
21
|
+
permissions:
|
|
22
|
+
id-token: write
|
|
23
|
+
contents: read
|
|
24
|
+
steps:
|
|
25
|
+
- uses: actions/checkout@v5
|
|
26
|
+
- uses: astral-sh/setup-uv@v6
|
|
27
|
+
- run: uv build
|
|
28
|
+
- uses: pypa/gh-action-pypi-publish@release/v1
|
|
29
|
+
|
|
30
|
+
ghcr:
|
|
31
|
+
needs: [test]
|
|
32
|
+
runs-on: ubuntu-latest
|
|
33
|
+
permissions:
|
|
34
|
+
contents: read
|
|
35
|
+
packages: write
|
|
36
|
+
steps:
|
|
37
|
+
- uses: actions/checkout@v5
|
|
38
|
+
- uses: docker/login-action@v3
|
|
39
|
+
with:
|
|
40
|
+
registry: ghcr.io
|
|
41
|
+
username: ${{ github.actor }}
|
|
42
|
+
password: ${{ secrets.GITHUB_TOKEN }}
|
|
43
|
+
- uses: docker/build-push-action@v6
|
|
44
|
+
with:
|
|
45
|
+
context: .
|
|
46
|
+
push: true
|
|
47
|
+
tags: |
|
|
48
|
+
ghcr.io/gronare/notes-vault-mcp:${{ github.ref_name }}
|
|
49
|
+
ghcr.io/gronare/notes-vault-mcp:latest
|
|
50
|
+
labels: io.modelcontextprotocol.server.name=io.github.gronare/notes-vault-mcp
|
|
51
|
+
|
|
52
|
+
registry:
|
|
53
|
+
needs: [pypi, ghcr]
|
|
54
|
+
runs-on: ubuntu-latest
|
|
55
|
+
permissions:
|
|
56
|
+
id-token: write
|
|
57
|
+
contents: read
|
|
58
|
+
steps:
|
|
59
|
+
- uses: actions/checkout@v5
|
|
60
|
+
|
|
61
|
+
- name: Match server.json to the tag
|
|
62
|
+
run: |
|
|
63
|
+
VERSION="${GITHUB_REF#refs/tags/v}"
|
|
64
|
+
jq --arg v "$VERSION" '.version = $v | .packages[0].version = $v' server.json > server.json.tmp
|
|
65
|
+
mv server.json.tmp server.json
|
|
66
|
+
|
|
67
|
+
- name: Install mcp-publisher
|
|
68
|
+
run: |
|
|
69
|
+
OS="$(uname -s | tr '[:upper:]' '[:lower:]')"
|
|
70
|
+
ARCH="$(uname -m | sed 's/x86_64/amd64/;s/aarch64/arm64/')"
|
|
71
|
+
curl -fsSL "https://github.com/modelcontextprotocol/registry/releases/latest/download/mcp-publisher_${OS}_${ARCH}.tar.gz" | tar xz mcp-publisher
|
|
72
|
+
|
|
73
|
+
- name: Log in to the MCP registry
|
|
74
|
+
run: ./mcp-publisher login github-oidc
|
|
75
|
+
|
|
76
|
+
- name: Publish
|
|
77
|
+
run: |
|
|
78
|
+
for attempt in 1 2 3 4 5; do
|
|
79
|
+
./mcp-publisher publish && exit 0
|
|
80
|
+
echo "publish failed, PyPI may not have the release yet; retrying in 30s"
|
|
81
|
+
sleep 30
|
|
82
|
+
done
|
|
83
|
+
exit 1
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
FROM ghcr.io/astral-sh/uv:python3.12-bookworm-slim
|
|
2
|
+
|
|
3
|
+
LABEL org.opencontainers.image.source="https://github.com/gronare/notes-vault-mcp"
|
|
4
|
+
LABEL io.modelcontextprotocol.server.name="io.github.gronare/notes-vault-mcp"
|
|
5
|
+
|
|
6
|
+
WORKDIR /app
|
|
7
|
+
ENV UV_COMPILE_BYTECODE=1 UV_LINK_MODE=copy PATH="/app/.venv/bin:$PATH"
|
|
8
|
+
|
|
9
|
+
COPY pyproject.toml uv.lock README.md ./
|
|
10
|
+
COPY notes_vault_mcp ./notes_vault_mcp
|
|
11
|
+
RUN uv sync --frozen --no-dev
|
|
12
|
+
|
|
13
|
+
ENTRYPOINT ["notes-vault-mcp"]
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Carl Green / Grönare
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: notes-vault-mcp
|
|
3
|
+
Version: 0.2.0
|
|
4
|
+
Summary: MCP server for a markdown notes vault on S3 or a local directory, with a SQLite index, schema-aware writes and session hooks.
|
|
5
|
+
Project-URL: Homepage, https://github.com/gronare/notes-vault-mcp
|
|
6
|
+
Project-URL: Repository, https://github.com/gronare/notes-vault-mcp
|
|
7
|
+
Author-email: Carl Green <carl.green@gronare.se>
|
|
8
|
+
License-Expression: MIT
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
Keywords: mcp,notes,obsidian,s3,vault
|
|
11
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
12
|
+
Classifier: Programming Language :: Python :: 3
|
|
13
|
+
Classifier: Topic :: Text Processing :: Markup :: Markdown
|
|
14
|
+
Requires-Python: >=3.11
|
|
15
|
+
Requires-Dist: boto3>=1.34.0
|
|
16
|
+
Requires-Dist: mcp>=1.16
|
|
17
|
+
Requires-Dist: pyyaml>=6.0
|
|
18
|
+
Description-Content-Type: text/markdown
|
|
19
|
+
|
|
20
|
+
<!-- mcp-name: io.github.gronare/notes-vault-mcp -->
|
|
21
|
+
|
|
22
|
+
# notes-vault-mcp
|
|
23
|
+
|
|
24
|
+
An MCP server for a vault of markdown notes — the kind Obsidian keeps: a folder of `.md` files with
|
|
25
|
+
YAML frontmatter. The vault lives either in a local directory or in an S3 bucket (MinIO included),
|
|
26
|
+
and the server gives an agent a cheap, indexed way to read and write it.
|
|
27
|
+
|
|
28
|
+
The point is that an agent should be able to answer "what do we already know about this?" in one
|
|
29
|
+
call, and should be told when the vault has drifted away from the code. So the server does more than
|
|
30
|
+
read and write files:
|
|
31
|
+
|
|
32
|
+
- **A local SQLite index.** Every tool call refreshes it, fetching only the notes whose version
|
|
33
|
+
changed. Search never downloads the vault.
|
|
34
|
+
- **Full-text search with BM25 ranking**, folder weights, recency decay and a status factor, so the
|
|
35
|
+
living system note outranks a two-year-old archived plan on the same words.
|
|
36
|
+
- **A schema.** The frontmatter contract lives in the vault as `.vault/schema.yml`: which folders
|
|
37
|
+
exist and what each is for, which fields are required, which statuses and kinds are legal, which
|
|
38
|
+
folders must link an `area`. Writes are validated against it and refused when they do not hold.
|
|
39
|
+
- **A lifecycle.** `close` archives a finished note and stamps its status; `log_append` writes one
|
|
40
|
+
dated line per repo per session; `lint` reports every kind of drift it can see.
|
|
41
|
+
- **Session hooks** for Claude Code: `session-start` hands the agent the system notes for the repo it
|
|
42
|
+
is about to touch — plus the commits made since each note was last updated — and `stop` refuses to
|
|
43
|
+
end a session that left commits unlogged or notes stale.
|
|
44
|
+
|
|
45
|
+
Swedish or English notes both work: the index folds diacritics, and the schema carries a synonym list
|
|
46
|
+
so `bokning` finds `booking`.
|
|
47
|
+
|
|
48
|
+
## Install
|
|
49
|
+
|
|
50
|
+
### As a Claude Code plugin
|
|
51
|
+
|
|
52
|
+
```sh
|
|
53
|
+
claude plugin marketplace add https://github.com/gronare/claude-plugins
|
|
54
|
+
claude plugin install vault@gronare
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
The plugin asks for the vault settings and passes them as `CLAUDE_PLUGIN_OPTION_*` environment
|
|
58
|
+
variables, which this server reads as if they were the bare names.
|
|
59
|
+
|
|
60
|
+
### As an MCP server, straight from PyPI
|
|
61
|
+
|
|
62
|
+
```sh
|
|
63
|
+
claude mcp add vault -s user \
|
|
64
|
+
-e VAULT_PATH=$HOME/vault \
|
|
65
|
+
-- uvx notes-vault-mcp
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
Or against S3 / MinIO:
|
|
69
|
+
|
|
70
|
+
```sh
|
|
71
|
+
claude mcp add vault -s user \
|
|
72
|
+
-e S3_ENDPOINT=https://minio.example.com \
|
|
73
|
+
-e S3_ACCESS_KEY=... \
|
|
74
|
+
-e S3_SECRET_KEY=... \
|
|
75
|
+
-e S3_BUCKET=vault \
|
|
76
|
+
-- uvx notes-vault-mcp
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
### As a container
|
|
80
|
+
|
|
81
|
+
```sh
|
|
82
|
+
claude mcp add vault -s user -- \
|
|
83
|
+
docker run --rm -i \
|
|
84
|
+
-e S3_ENDPOINT -e S3_ACCESS_KEY -e S3_SECRET_KEY -e S3_BUCKET \
|
|
85
|
+
ghcr.io/gronare/notes-vault-mcp:latest
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
## Configuration
|
|
89
|
+
|
|
90
|
+
Every variable is also read from `CLAUDE_PLUGIN_OPTION_<NAME>`, which is how the Claude Code plugin
|
|
91
|
+
passes its user config. The bare name wins when both are set.
|
|
92
|
+
|
|
93
|
+
| Variable | Required | Meaning |
|
|
94
|
+
| --- | --- | --- |
|
|
95
|
+
| `VAULT_PATH` | for a local vault | Directory holding the vault. Selects the local backend. |
|
|
96
|
+
| `S3_ENDPOINT` | for an S3 vault | Endpoint URL, e.g. `https://minio.example.com`. |
|
|
97
|
+
| `S3_ACCESS_KEY` | for an S3 vault | Access key. |
|
|
98
|
+
| `S3_SECRET_KEY` | for an S3 vault | Secret key. |
|
|
99
|
+
| `S3_BUCKET` | for an S3 vault | Bucket holding the vault. |
|
|
100
|
+
| `S3_PREFIX` | no | Key prefix inside the bucket. |
|
|
101
|
+
| `S3_REGION` | no | Region, default `us-east-1`. |
|
|
102
|
+
| `VAULT_CACHE_DIR` | no | Where the index lives, default `~/.cache/notes-vault-mcp`. |
|
|
103
|
+
| `VAULT_SCHEMA` | no | Local path to a schema file, overriding the one in the vault. |
|
|
104
|
+
| `VAULT_TOKEN` | for HTTP | Bearer token. Required by `--transport http`. |
|
|
105
|
+
| `VAULT_STOP_HOOK` | no | `off` disables the stop hook. |
|
|
106
|
+
|
|
107
|
+
Set `VAULT_PATH` **or** the four `S3_*` variables. With neither, the server exits with one line
|
|
108
|
+
saying so.
|
|
109
|
+
|
|
110
|
+
## First run
|
|
111
|
+
|
|
112
|
+
```sh
|
|
113
|
+
uvx notes-vault-mcp init
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
`init` writes into the vault, and refuses to overwrite anything without `--force`:
|
|
117
|
+
|
|
118
|
+
- `.vault/schema.yml` — the frontmatter contract, copied from the built-in default so you can edit it.
|
|
119
|
+
- `Areas.base`, `Open tasks.base`, `Resources.base` — Obsidian Bases views over the same structure.
|
|
120
|
+
|
|
121
|
+
It then prints a CLAUDE.md snippet to stdout: the workflow rules an agent needs on its side of the
|
|
122
|
+
conversation.
|
|
123
|
+
|
|
124
|
+
## The schema
|
|
125
|
+
|
|
126
|
+
`.vault/schema.yml` is deep-merged over the built-in default, so it only needs to carry what differs.
|
|
127
|
+
The default lays out five folders:
|
|
128
|
+
|
|
129
|
+
| Folder | Kind | Weight | Role |
|
|
130
|
+
| --- | --- | --- | --- |
|
|
131
|
+
| `Areas/` | system | 3.0 | One living note per system. Current state only. The hubs of the graph. |
|
|
132
|
+
| `Resources/` | reference | 2.0 | Traps, how-tos and decisions with their reasons. |
|
|
133
|
+
| `Projects/` | task | 1.0 | Open work spanning sessions. Closed with `close`. |
|
|
134
|
+
| `Log/` | log | 1.0 | Append-only log per repo, one note per repo. |
|
|
135
|
+
| `Archive/` | archive | 0.3 | History. Searched only on request. |
|
|
136
|
+
|
|
137
|
+
and the contract for a note:
|
|
138
|
+
|
|
139
|
+
```yaml
|
|
140
|
+
frontmatter:
|
|
141
|
+
required: [title, date, updated, tags, status]
|
|
142
|
+
optional: [kind, area, summary, path, superseded_by]
|
|
143
|
+
area_required_in: [Projects, Resources, Log]
|
|
144
|
+
status_values: [draft, active, complete, superseded]
|
|
145
|
+
kind_values: [system, task, trap, howto, decision, reference, log]
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
`path` is what ties a note to code: a comma-separated list of directories (`~` is kept as written and
|
|
149
|
+
also indexed expanded). That is what `context` and the session hook match against.
|
|
150
|
+
|
|
151
|
+
The repo log is one note per repo, and both its filename and its line format are schema settings:
|
|
152
|
+
|
|
153
|
+
```yaml
|
|
154
|
+
log:
|
|
155
|
+
folder: Log
|
|
156
|
+
file_format: "{repo}-log.md"
|
|
157
|
+
entry_format: "- [{date}] {line} | commits: {commits} | {area}"
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
`file_format` takes a single `{repo}` placeholder, and the default suffix is what keeps the log clear
|
|
161
|
+
of the hub note: with `Areas/greenhouse.md` and `Log/greenhouse.md` both in the vault, Obsidian cannot
|
|
162
|
+
resolve `[[greenhouse]]`. Every place that builds the log path reads this setting — `log_append`, the
|
|
163
|
+
log tail in `context`, the stop hook's unlogged-commit check, `changelog` and `lint` — so changing it
|
|
164
|
+
moves all of them at once. Rename the existing files to match when you change it.
|
|
165
|
+
|
|
166
|
+
Also configurable: the tag vocabulary and whether it is enforced, the synonym groups search expands,
|
|
167
|
+
`stale_after_days`, and the search weights.
|
|
168
|
+
|
|
169
|
+
## Tools
|
|
170
|
+
|
|
171
|
+
Every call refreshes the index first, throttled to at most once every 20 seconds.
|
|
172
|
+
|
|
173
|
+
| Tool | Cost | What it does |
|
|
174
|
+
| --- | --- | --- |
|
|
175
|
+
| `search` | cheap | Full-text over the index. Title, summary, tags and body, with synonyms, prefixes, quoted phrases and folded diacritics. A bare commit sha finds the notes that mention it. Hides archive and superseded notes and says how many. |
|
|
176
|
+
| `context` | cheap | The session-start call: the system notes covering a path, the open tasks, the reference notes and the tail of the repo log, in one answer. |
|
|
177
|
+
| `list_files` | cheap | Paths only. |
|
|
178
|
+
| `read_file` | moderate | One note, prefixed with `etag: <version>`. A superseded note carries a warning callout. |
|
|
179
|
+
| `lint` | moderate | Reads every note and reports drift. |
|
|
180
|
+
| `write_file` | write | Validates against the schema and refuses the write if it does not hold. Stamps `updated`, fills `date`. Pass `expected_etag` to make the write conditional. |
|
|
181
|
+
| `append_file` | write | Appends and bumps `updated`. Creates the note when missing. |
|
|
182
|
+
| `close` | write | Sets status complete (or superseded, with `superseded_by`, when `merged_into` is given) and moves the note into the archive. |
|
|
183
|
+
| `log_append` | write | One dated line in the repo log, with the commits it produced. Creates the log when missing. |
|
|
184
|
+
| `move_file` | write | Moves or renames. |
|
|
185
|
+
| `delete_file` | write | Deletes for good. Prefer `close`. |
|
|
186
|
+
|
|
187
|
+
`search` filters: `folder`, `status`, `tag`, `kind`, `area`, `path_prefix`, `since`,
|
|
188
|
+
`include_archive`, `include_superseded`, `limit`.
|
|
189
|
+
|
|
190
|
+
### What lint reports
|
|
191
|
+
|
|
192
|
+
`broken_frontmatter`, `missing_required` (per field), `missing_area`, `unknown_tags` (only when the
|
|
193
|
+
vocabulary is strict), `unresolved_links`, `orphans` (no inbound wikilink; log and archive ignored),
|
|
194
|
+
`stale_active`, `archive_status_mismatch`, `duplicate_stems`, `superseded_target_missing`.
|
|
195
|
+
|
|
196
|
+
```sh
|
|
197
|
+
uvx notes-vault-mcp lint
|
|
198
|
+
uvx notes-vault-mcp lint --write "Log/lint-$(date +%F).md"
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
## Hooks
|
|
202
|
+
|
|
203
|
+
Two Claude Code hooks, both reading the hook JSON on stdin and both exiting 0 whatever happens.
|
|
204
|
+
|
|
205
|
+
`session-start` prints the context bundle for the working directory, then — for each system note it
|
|
206
|
+
returned — the commits touching that note's `path` since the note was last updated. That is the
|
|
207
|
+
answer to "is this note still true?" before the agent believes it.
|
|
208
|
+
|
|
209
|
+
`stop` blocks the end of a session that left work unrecorded: commits from the last 24 hours whose
|
|
210
|
+
sha does not appear in the repo log, and open task notes older than 14 days. It returns
|
|
211
|
+
`{"decision": "block", "reason": ...}`, or nothing at all when the vault is up to date. Set
|
|
212
|
+
`VAULT_STOP_HOOK=off` to silence it.
|
|
213
|
+
|
|
214
|
+
```json
|
|
215
|
+
{
|
|
216
|
+
"hooks": {
|
|
217
|
+
"SessionStart": [
|
|
218
|
+
{ "hooks": [{ "type": "command", "command": "uvx notes-vault-mcp hook session-start" }] }
|
|
219
|
+
],
|
|
220
|
+
"Stop": [
|
|
221
|
+
{ "hooks": [{ "type": "command", "command": "uvx notes-vault-mcp hook stop" }] }
|
|
222
|
+
]
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
## Other commands
|
|
228
|
+
|
|
229
|
+
```sh
|
|
230
|
+
notes-vault-mcp serve --transport stdio # the default
|
|
231
|
+
notes-vault-mcp sync --rebuild # drop the index and read every note again
|
|
232
|
+
notes-vault-mcp search "bokning" --limit 5 # the same ranking, from a shell
|
|
233
|
+
notes-vault-mcp changelog greenhouse 2026-08 --repo-path ~/projects/greenhouse
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
`changelog` prints the log lines, the git commits grouped by day, and the notes dated inside the
|
|
237
|
+
period. It writes nothing; it is input for an agent compiling a month page.
|
|
238
|
+
|
|
239
|
+
## HTTP transport
|
|
240
|
+
|
|
241
|
+
```sh
|
|
242
|
+
VAULT_TOKEN=$(openssl rand -hex 32) notes-vault-mcp serve --transport http --host 0.0.0.0 --port 8765
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
Streamable HTTP on `/mcp`. Every request must carry `Authorization: Bearer $VAULT_TOKEN`; anything
|
|
246
|
+
else gets 401 before it reaches the server. `VAULT_TOKEN` is mandatory in this mode — the command
|
|
247
|
+
refuses to start without it.
|
|
248
|
+
|
|
249
|
+
## Development
|
|
250
|
+
|
|
251
|
+
```sh
|
|
252
|
+
uv sync
|
|
253
|
+
uv run pytest
|
|
254
|
+
uv run ruff check .
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
The test suite runs against a fixture vault under `tests/fixtures/vault/` and a moto-mocked S3
|
|
258
|
+
bucket. It never touches a real bucket.
|
|
259
|
+
|
|
260
|
+
## License
|
|
261
|
+
|
|
262
|
+
MIT. See [LICENSE](LICENSE).
|
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
<!-- mcp-name: io.github.gronare/notes-vault-mcp -->
|
|
2
|
+
|
|
3
|
+
# notes-vault-mcp
|
|
4
|
+
|
|
5
|
+
An MCP server for a vault of markdown notes — the kind Obsidian keeps: a folder of `.md` files with
|
|
6
|
+
YAML frontmatter. The vault lives either in a local directory or in an S3 bucket (MinIO included),
|
|
7
|
+
and the server gives an agent a cheap, indexed way to read and write it.
|
|
8
|
+
|
|
9
|
+
The point is that an agent should be able to answer "what do we already know about this?" in one
|
|
10
|
+
call, and should be told when the vault has drifted away from the code. So the server does more than
|
|
11
|
+
read and write files:
|
|
12
|
+
|
|
13
|
+
- **A local SQLite index.** Every tool call refreshes it, fetching only the notes whose version
|
|
14
|
+
changed. Search never downloads the vault.
|
|
15
|
+
- **Full-text search with BM25 ranking**, folder weights, recency decay and a status factor, so the
|
|
16
|
+
living system note outranks a two-year-old archived plan on the same words.
|
|
17
|
+
- **A schema.** The frontmatter contract lives in the vault as `.vault/schema.yml`: which folders
|
|
18
|
+
exist and what each is for, which fields are required, which statuses and kinds are legal, which
|
|
19
|
+
folders must link an `area`. Writes are validated against it and refused when they do not hold.
|
|
20
|
+
- **A lifecycle.** `close` archives a finished note and stamps its status; `log_append` writes one
|
|
21
|
+
dated line per repo per session; `lint` reports every kind of drift it can see.
|
|
22
|
+
- **Session hooks** for Claude Code: `session-start` hands the agent the system notes for the repo it
|
|
23
|
+
is about to touch — plus the commits made since each note was last updated — and `stop` refuses to
|
|
24
|
+
end a session that left commits unlogged or notes stale.
|
|
25
|
+
|
|
26
|
+
Swedish or English notes both work: the index folds diacritics, and the schema carries a synonym list
|
|
27
|
+
so `bokning` finds `booking`.
|
|
28
|
+
|
|
29
|
+
## Install
|
|
30
|
+
|
|
31
|
+
### As a Claude Code plugin
|
|
32
|
+
|
|
33
|
+
```sh
|
|
34
|
+
claude plugin marketplace add https://github.com/gronare/claude-plugins
|
|
35
|
+
claude plugin install vault@gronare
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
The plugin asks for the vault settings and passes them as `CLAUDE_PLUGIN_OPTION_*` environment
|
|
39
|
+
variables, which this server reads as if they were the bare names.
|
|
40
|
+
|
|
41
|
+
### As an MCP server, straight from PyPI
|
|
42
|
+
|
|
43
|
+
```sh
|
|
44
|
+
claude mcp add vault -s user \
|
|
45
|
+
-e VAULT_PATH=$HOME/vault \
|
|
46
|
+
-- uvx notes-vault-mcp
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
Or against S3 / MinIO:
|
|
50
|
+
|
|
51
|
+
```sh
|
|
52
|
+
claude mcp add vault -s user \
|
|
53
|
+
-e S3_ENDPOINT=https://minio.example.com \
|
|
54
|
+
-e S3_ACCESS_KEY=... \
|
|
55
|
+
-e S3_SECRET_KEY=... \
|
|
56
|
+
-e S3_BUCKET=vault \
|
|
57
|
+
-- uvx notes-vault-mcp
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
### As a container
|
|
61
|
+
|
|
62
|
+
```sh
|
|
63
|
+
claude mcp add vault -s user -- \
|
|
64
|
+
docker run --rm -i \
|
|
65
|
+
-e S3_ENDPOINT -e S3_ACCESS_KEY -e S3_SECRET_KEY -e S3_BUCKET \
|
|
66
|
+
ghcr.io/gronare/notes-vault-mcp:latest
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
## Configuration
|
|
70
|
+
|
|
71
|
+
Every variable is also read from `CLAUDE_PLUGIN_OPTION_<NAME>`, which is how the Claude Code plugin
|
|
72
|
+
passes its user config. The bare name wins when both are set.
|
|
73
|
+
|
|
74
|
+
| Variable | Required | Meaning |
|
|
75
|
+
| --- | --- | --- |
|
|
76
|
+
| `VAULT_PATH` | for a local vault | Directory holding the vault. Selects the local backend. |
|
|
77
|
+
| `S3_ENDPOINT` | for an S3 vault | Endpoint URL, e.g. `https://minio.example.com`. |
|
|
78
|
+
| `S3_ACCESS_KEY` | for an S3 vault | Access key. |
|
|
79
|
+
| `S3_SECRET_KEY` | for an S3 vault | Secret key. |
|
|
80
|
+
| `S3_BUCKET` | for an S3 vault | Bucket holding the vault. |
|
|
81
|
+
| `S3_PREFIX` | no | Key prefix inside the bucket. |
|
|
82
|
+
| `S3_REGION` | no | Region, default `us-east-1`. |
|
|
83
|
+
| `VAULT_CACHE_DIR` | no | Where the index lives, default `~/.cache/notes-vault-mcp`. |
|
|
84
|
+
| `VAULT_SCHEMA` | no | Local path to a schema file, overriding the one in the vault. |
|
|
85
|
+
| `VAULT_TOKEN` | for HTTP | Bearer token. Required by `--transport http`. |
|
|
86
|
+
| `VAULT_STOP_HOOK` | no | `off` disables the stop hook. |
|
|
87
|
+
|
|
88
|
+
Set `VAULT_PATH` **or** the four `S3_*` variables. With neither, the server exits with one line
|
|
89
|
+
saying so.
|
|
90
|
+
|
|
91
|
+
## First run
|
|
92
|
+
|
|
93
|
+
```sh
|
|
94
|
+
uvx notes-vault-mcp init
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
`init` writes into the vault, and refuses to overwrite anything without `--force`:
|
|
98
|
+
|
|
99
|
+
- `.vault/schema.yml` — the frontmatter contract, copied from the built-in default so you can edit it.
|
|
100
|
+
- `Areas.base`, `Open tasks.base`, `Resources.base` — Obsidian Bases views over the same structure.
|
|
101
|
+
|
|
102
|
+
It then prints a CLAUDE.md snippet to stdout: the workflow rules an agent needs on its side of the
|
|
103
|
+
conversation.
|
|
104
|
+
|
|
105
|
+
## The schema
|
|
106
|
+
|
|
107
|
+
`.vault/schema.yml` is deep-merged over the built-in default, so it only needs to carry what differs.
|
|
108
|
+
The default lays out five folders:
|
|
109
|
+
|
|
110
|
+
| Folder | Kind | Weight | Role |
|
|
111
|
+
| --- | --- | --- | --- |
|
|
112
|
+
| `Areas/` | system | 3.0 | One living note per system. Current state only. The hubs of the graph. |
|
|
113
|
+
| `Resources/` | reference | 2.0 | Traps, how-tos and decisions with their reasons. |
|
|
114
|
+
| `Projects/` | task | 1.0 | Open work spanning sessions. Closed with `close`. |
|
|
115
|
+
| `Log/` | log | 1.0 | Append-only log per repo, one note per repo. |
|
|
116
|
+
| `Archive/` | archive | 0.3 | History. Searched only on request. |
|
|
117
|
+
|
|
118
|
+
and the contract for a note:
|
|
119
|
+
|
|
120
|
+
```yaml
|
|
121
|
+
frontmatter:
|
|
122
|
+
required: [title, date, updated, tags, status]
|
|
123
|
+
optional: [kind, area, summary, path, superseded_by]
|
|
124
|
+
area_required_in: [Projects, Resources, Log]
|
|
125
|
+
status_values: [draft, active, complete, superseded]
|
|
126
|
+
kind_values: [system, task, trap, howto, decision, reference, log]
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
`path` is what ties a note to code: a comma-separated list of directories (`~` is kept as written and
|
|
130
|
+
also indexed expanded). That is what `context` and the session hook match against.
|
|
131
|
+
|
|
132
|
+
The repo log is one note per repo, and both its filename and its line format are schema settings:
|
|
133
|
+
|
|
134
|
+
```yaml
|
|
135
|
+
log:
|
|
136
|
+
folder: Log
|
|
137
|
+
file_format: "{repo}-log.md"
|
|
138
|
+
entry_format: "- [{date}] {line} | commits: {commits} | {area}"
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
`file_format` takes a single `{repo}` placeholder, and the default suffix is what keeps the log clear
|
|
142
|
+
of the hub note: with `Areas/greenhouse.md` and `Log/greenhouse.md` both in the vault, Obsidian cannot
|
|
143
|
+
resolve `[[greenhouse]]`. Every place that builds the log path reads this setting — `log_append`, the
|
|
144
|
+
log tail in `context`, the stop hook's unlogged-commit check, `changelog` and `lint` — so changing it
|
|
145
|
+
moves all of them at once. Rename the existing files to match when you change it.
|
|
146
|
+
|
|
147
|
+
Also configurable: the tag vocabulary and whether it is enforced, the synonym groups search expands,
|
|
148
|
+
`stale_after_days`, and the search weights.
|
|
149
|
+
|
|
150
|
+
## Tools
|
|
151
|
+
|
|
152
|
+
Every call refreshes the index first, throttled to at most once every 20 seconds.
|
|
153
|
+
|
|
154
|
+
| Tool | Cost | What it does |
|
|
155
|
+
| --- | --- | --- |
|
|
156
|
+
| `search` | cheap | Full-text over the index. Title, summary, tags and body, with synonyms, prefixes, quoted phrases and folded diacritics. A bare commit sha finds the notes that mention it. Hides archive and superseded notes and says how many. |
|
|
157
|
+
| `context` | cheap | The session-start call: the system notes covering a path, the open tasks, the reference notes and the tail of the repo log, in one answer. |
|
|
158
|
+
| `list_files` | cheap | Paths only. |
|
|
159
|
+
| `read_file` | moderate | One note, prefixed with `etag: <version>`. A superseded note carries a warning callout. |
|
|
160
|
+
| `lint` | moderate | Reads every note and reports drift. |
|
|
161
|
+
| `write_file` | write | Validates against the schema and refuses the write if it does not hold. Stamps `updated`, fills `date`. Pass `expected_etag` to make the write conditional. |
|
|
162
|
+
| `append_file` | write | Appends and bumps `updated`. Creates the note when missing. |
|
|
163
|
+
| `close` | write | Sets status complete (or superseded, with `superseded_by`, when `merged_into` is given) and moves the note into the archive. |
|
|
164
|
+
| `log_append` | write | One dated line in the repo log, with the commits it produced. Creates the log when missing. |
|
|
165
|
+
| `move_file` | write | Moves or renames. |
|
|
166
|
+
| `delete_file` | write | Deletes for good. Prefer `close`. |
|
|
167
|
+
|
|
168
|
+
`search` filters: `folder`, `status`, `tag`, `kind`, `area`, `path_prefix`, `since`,
|
|
169
|
+
`include_archive`, `include_superseded`, `limit`.
|
|
170
|
+
|
|
171
|
+
### What lint reports
|
|
172
|
+
|
|
173
|
+
`broken_frontmatter`, `missing_required` (per field), `missing_area`, `unknown_tags` (only when the
|
|
174
|
+
vocabulary is strict), `unresolved_links`, `orphans` (no inbound wikilink; log and archive ignored),
|
|
175
|
+
`stale_active`, `archive_status_mismatch`, `duplicate_stems`, `superseded_target_missing`.
|
|
176
|
+
|
|
177
|
+
```sh
|
|
178
|
+
uvx notes-vault-mcp lint
|
|
179
|
+
uvx notes-vault-mcp lint --write "Log/lint-$(date +%F).md"
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
## Hooks
|
|
183
|
+
|
|
184
|
+
Two Claude Code hooks, both reading the hook JSON on stdin and both exiting 0 whatever happens.
|
|
185
|
+
|
|
186
|
+
`session-start` prints the context bundle for the working directory, then — for each system note it
|
|
187
|
+
returned — the commits touching that note's `path` since the note was last updated. That is the
|
|
188
|
+
answer to "is this note still true?" before the agent believes it.
|
|
189
|
+
|
|
190
|
+
`stop` blocks the end of a session that left work unrecorded: commits from the last 24 hours whose
|
|
191
|
+
sha does not appear in the repo log, and open task notes older than 14 days. It returns
|
|
192
|
+
`{"decision": "block", "reason": ...}`, or nothing at all when the vault is up to date. Set
|
|
193
|
+
`VAULT_STOP_HOOK=off` to silence it.
|
|
194
|
+
|
|
195
|
+
```json
|
|
196
|
+
{
|
|
197
|
+
"hooks": {
|
|
198
|
+
"SessionStart": [
|
|
199
|
+
{ "hooks": [{ "type": "command", "command": "uvx notes-vault-mcp hook session-start" }] }
|
|
200
|
+
],
|
|
201
|
+
"Stop": [
|
|
202
|
+
{ "hooks": [{ "type": "command", "command": "uvx notes-vault-mcp hook stop" }] }
|
|
203
|
+
]
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
## Other commands
|
|
209
|
+
|
|
210
|
+
```sh
|
|
211
|
+
notes-vault-mcp serve --transport stdio # the default
|
|
212
|
+
notes-vault-mcp sync --rebuild # drop the index and read every note again
|
|
213
|
+
notes-vault-mcp search "bokning" --limit 5 # the same ranking, from a shell
|
|
214
|
+
notes-vault-mcp changelog greenhouse 2026-08 --repo-path ~/projects/greenhouse
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
`changelog` prints the log lines, the git commits grouped by day, and the notes dated inside the
|
|
218
|
+
period. It writes nothing; it is input for an agent compiling a month page.
|
|
219
|
+
|
|
220
|
+
## HTTP transport
|
|
221
|
+
|
|
222
|
+
```sh
|
|
223
|
+
VAULT_TOKEN=$(openssl rand -hex 32) notes-vault-mcp serve --transport http --host 0.0.0.0 --port 8765
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
Streamable HTTP on `/mcp`. Every request must carry `Authorization: Bearer $VAULT_TOKEN`; anything
|
|
227
|
+
else gets 401 before it reaches the server. `VAULT_TOKEN` is mandatory in this mode — the command
|
|
228
|
+
refuses to start without it.
|
|
229
|
+
|
|
230
|
+
## Development
|
|
231
|
+
|
|
232
|
+
```sh
|
|
233
|
+
uv sync
|
|
234
|
+
uv run pytest
|
|
235
|
+
uv run ruff check .
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
The test suite runs against a fixture vault under `tests/fixtures/vault/` and a moto-mocked S3
|
|
239
|
+
bucket. It never touches a real bucket.
|
|
240
|
+
|
|
241
|
+
## License
|
|
242
|
+
|
|
243
|
+
MIT. See [LICENSE](LICENSE).
|