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.
Files changed (58) hide show
  1. notes_vault_mcp-0.2.0/.dockerignore +8 -0
  2. notes_vault_mcp-0.2.0/.github/workflows/ci.yml +22 -0
  3. notes_vault_mcp-0.2.0/.github/workflows/release.yml +83 -0
  4. notes_vault_mcp-0.2.0/.gitignore +11 -0
  5. notes_vault_mcp-0.2.0/Dockerfile +13 -0
  6. notes_vault_mcp-0.2.0/LICENSE +21 -0
  7. notes_vault_mcp-0.2.0/PKG-INFO +262 -0
  8. notes_vault_mcp-0.2.0/README.md +243 -0
  9. notes_vault_mcp-0.2.0/notes_vault_mcp/__init__.py +3 -0
  10. notes_vault_mcp-0.2.0/notes_vault_mcp/backends/__init__.py +70 -0
  11. notes_vault_mcp-0.2.0/notes_vault_mcp/backends/local.py +79 -0
  12. notes_vault_mcp-0.2.0/notes_vault_mcp/backends/s3.py +106 -0
  13. notes_vault_mcp-0.2.0/notes_vault_mcp/changelog.py +54 -0
  14. notes_vault_mcp-0.2.0/notes_vault_mcp/cli.py +162 -0
  15. notes_vault_mcp-0.2.0/notes_vault_mcp/config.py +58 -0
  16. notes_vault_mcp-0.2.0/notes_vault_mcp/frontmatter.py +121 -0
  17. notes_vault_mcp-0.2.0/notes_vault_mcp/hooks.py +150 -0
  18. notes_vault_mcp-0.2.0/notes_vault_mcp/index.py +268 -0
  19. notes_vault_mcp-0.2.0/notes_vault_mcp/notes.py +420 -0
  20. notes_vault_mcp-0.2.0/notes_vault_mcp/schema.py +239 -0
  21. notes_vault_mcp-0.2.0/notes_vault_mcp/search.py +235 -0
  22. notes_vault_mcp-0.2.0/notes_vault_mcp/server.py +201 -0
  23. notes_vault_mcp-0.2.0/notes_vault_mcp/templates/Areas.base +19 -0
  24. notes_vault_mcp-0.2.0/notes_vault_mcp/templates/Open tasks.base +26 -0
  25. notes_vault_mcp-0.2.0/notes_vault_mcp/templates/Resources.base +22 -0
  26. notes_vault_mcp-0.2.0/notes_vault_mcp/templates/__init__.py +0 -0
  27. notes_vault_mcp-0.2.0/notes_vault_mcp/templates/claude-md-snippet.md +15 -0
  28. notes_vault_mcp-0.2.0/notes_vault_mcp/templates/schema.yml +89 -0
  29. notes_vault_mcp-0.2.0/notes_vault_mcp/vault.py +26 -0
  30. notes_vault_mcp-0.2.0/pyproject.toml +50 -0
  31. notes_vault_mcp-0.2.0/server.json +103 -0
  32. notes_vault_mcp-0.2.0/tests/conftest.py +46 -0
  33. notes_vault_mcp-0.2.0/tests/fixtures/real-queries.json +567 -0
  34. notes_vault_mcp-0.2.0/tests/fixtures/vault/.obsidian/app.json +1 -0
  35. notes_vault_mcp-0.2.0/tests/fixtures/vault/.vault/schema.yml +5 -0
  36. notes_vault_mcp-0.2.0/tests/fixtures/vault/Archive/old-plan.md +11 -0
  37. notes_vault_mcp-0.2.0/tests/fixtures/vault/Archive/wrong-status.md +11 -0
  38. notes_vault_mcp-0.2.0/tests/fixtures/vault/Areas/greenhouse.md +15 -0
  39. notes_vault_mcp-0.2.0/tests/fixtures/vault/Areas/homelab.md +13 -0
  40. notes_vault_mcp-0.2.0/tests/fixtures/vault/Log/greenhouse-log.md +12 -0
  41. notes_vault_mcp-0.2.0/tests/fixtures/vault/Projects/greenhouse-draft.md +13 -0
  42. notes_vault_mcp-0.2.0/tests/fixtures/vault/Projects/greenhouse-fresh.md +13 -0
  43. notes_vault_mcp-0.2.0/tests/fixtures/vault/Projects/homelab-stale.md +12 -0
  44. notes_vault_mcp-0.2.0/tests/fixtures/vault/Resources/booking-trap.md +13 -0
  45. notes_vault_mcp-0.2.0/tests/fixtures/vault/Resources/broken-yaml.md +7 -0
  46. notes_vault_mcp-0.2.0/tests/fixtures/vault/Resources/homelab.md +11 -0
  47. notes_vault_mcp-0.2.0/tests/fixtures/vault/Resources/no-frontmatter.md +1 -0
  48. notes_vault_mcp-0.2.0/tests/fixtures/vault/Resources/old-domain-howto.md +12 -0
  49. notes_vault_mcp-0.2.0/tests/test_backends.py +109 -0
  50. notes_vault_mcp-0.2.0/tests/test_cli_hooks.py +214 -0
  51. notes_vault_mcp-0.2.0/tests/test_frontmatter.py +113 -0
  52. notes_vault_mcp-0.2.0/tests/test_index.py +95 -0
  53. notes_vault_mcp-0.2.0/tests/test_lint.py +78 -0
  54. notes_vault_mcp-0.2.0/tests/test_notes.py +215 -0
  55. notes_vault_mcp-0.2.0/tests/test_real_queries.py +32 -0
  56. notes_vault_mcp-0.2.0/tests/test_search.py +107 -0
  57. notes_vault_mcp-0.2.0/tests/test_server.py +127 -0
  58. notes_vault_mcp-0.2.0/uv.lock +1262 -0
@@ -0,0 +1,8 @@
1
+ .git
2
+ .venv
3
+ tests
4
+ dist
5
+ build
6
+ __pycache__
7
+ .pytest_cache
8
+ .ruff_cache
@@ -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,11 @@
1
+ .venv/
2
+ __pycache__/
3
+ *.pyc
4
+ .env
5
+ *.sqlite
6
+ *.sqlite-shm
7
+ *.sqlite-wal
8
+ dist/
9
+ build/
10
+ .pytest_cache/
11
+ .ruff_cache/
@@ -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).
@@ -0,0 +1,3 @@
1
+ __all__ = ["__version__"]
2
+
3
+ __version__ = "0.2.0"