obsidian-gateway 0.5.1__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 (50) hide show
  1. obsidian_gateway-0.5.1/.github/dependabot.yml +20 -0
  2. obsidian_gateway-0.5.1/.github/workflows/ci.yml +35 -0
  3. obsidian_gateway-0.5.1/.github/workflows/release.yml +34 -0
  4. obsidian_gateway-0.5.1/.gitignore +12 -0
  5. obsidian_gateway-0.5.1/CHANGELOG.md +97 -0
  6. obsidian_gateway-0.5.1/LICENSE +21 -0
  7. obsidian_gateway-0.5.1/PKG-INFO +249 -0
  8. obsidian_gateway-0.5.1/README.md +223 -0
  9. obsidian_gateway-0.5.1/conftest.py +2 -0
  10. obsidian_gateway-0.5.1/deploy/auto-update.sh +11 -0
  11. obsidian_gateway-0.5.1/deploy/obsidian-gateway-update.service +13 -0
  12. obsidian_gateway-0.5.1/deploy/obsidian-gateway-update.timer +10 -0
  13. obsidian_gateway-0.5.1/deploy/obsidian-gateway.service +54 -0
  14. obsidian_gateway-0.5.1/deploy/tailscale.md +42 -0
  15. obsidian_gateway-0.5.1/gateway/__init__.py +7 -0
  16. obsidian_gateway-0.5.1/gateway/__main__.py +3 -0
  17. obsidian_gateway-0.5.1/gateway/acl.py +62 -0
  18. obsidian_gateway-0.5.1/gateway/config.py +79 -0
  19. obsidian_gateway-0.5.1/gateway/detect.py +49 -0
  20. obsidian_gateway-0.5.1/gateway/edits.py +160 -0
  21. obsidian_gateway-0.5.1/gateway/gitops.py +85 -0
  22. obsidian_gateway-0.5.1/gateway/links.py +22 -0
  23. obsidian_gateway-0.5.1/gateway/locks.py +74 -0
  24. obsidian_gateway-0.5.1/gateway/search.py +94 -0
  25. obsidian_gateway-0.5.1/gateway/server.py +98 -0
  26. obsidian_gateway-0.5.1/gateway/tags.py +20 -0
  27. obsidian_gateway-0.5.1/gateway/tools.py +409 -0
  28. obsidian_gateway-0.5.1/gateway/vaults.py +147 -0
  29. obsidian_gateway-0.5.1/gateway/writes.py +31 -0
  30. obsidian_gateway-0.5.1/pyproject.toml +48 -0
  31. obsidian_gateway-0.5.1/server.json +19 -0
  32. obsidian_gateway-0.5.1/tests/conftest.py +32 -0
  33. obsidian_gateway-0.5.1/tests/test_acl.py +60 -0
  34. obsidian_gateway-0.5.1/tests/test_attachments.py +49 -0
  35. obsidian_gateway-0.5.1/tests/test_canvas.py +44 -0
  36. obsidian_gateway-0.5.1/tests/test_config.py +104 -0
  37. obsidian_gateway-0.5.1/tests/test_detect.py +61 -0
  38. obsidian_gateway-0.5.1/tests/test_edits.py +126 -0
  39. obsidian_gateway-0.5.1/tests/test_gitops.py +53 -0
  40. obsidian_gateway-0.5.1/tests/test_local.py +33 -0
  41. obsidian_gateway-0.5.1/tests/test_locks.py +84 -0
  42. obsidian_gateway-0.5.1/tests/test_masking.py +41 -0
  43. obsidian_gateway-0.5.1/tests/test_search.py +23 -0
  44. obsidian_gateway-0.5.1/tests/test_security.py +39 -0
  45. obsidian_gateway-0.5.1/tests/test_tools.py +82 -0
  46. obsidian_gateway-0.5.1/tests/test_vaults.py +112 -0
  47. obsidian_gateway-0.5.1/tests/test_writes.py +48 -0
  48. obsidian_gateway-0.5.1/tokens.example.yaml +27 -0
  49. obsidian_gateway-0.5.1/uv.lock +1689 -0
  50. obsidian_gateway-0.5.1/vaults.example.yaml +23 -0
@@ -0,0 +1,20 @@
1
+ version: 2
2
+ # Keep the SHA-pinned GitHub Actions and the Python/uv dependency tree current.
3
+ # Dependabot opens small PRs; CI (and branch protection) gate them.
4
+ updates:
5
+ - package-ecosystem: github-actions
6
+ directory: /
7
+ schedule:
8
+ interval: weekly
9
+ commit-message:
10
+ prefix: ci
11
+ groups:
12
+ actions:
13
+ patterns: ["*"]
14
+
15
+ - package-ecosystem: uv
16
+ directory: /
17
+ schedule:
18
+ interval: weekly
19
+ commit-message:
20
+ prefix: deps
@@ -0,0 +1,35 @@
1
+ name: ci
2
+
3
+ # Test on every PR and on pushes to main (so the published default branch and
4
+ # every release tag are known-green). No secrets, no network beyond pip.
5
+ on:
6
+ pull_request:
7
+ push:
8
+ branches: [main]
9
+ tags: ['v*']
10
+
11
+ permissions:
12
+ contents: read
13
+
14
+ concurrency:
15
+ group: ${{ github.workflow }}-${{ github.ref }}
16
+ cancel-in-progress: true
17
+
18
+ jobs:
19
+ test:
20
+ runs-on: ubuntu-latest
21
+ strategy:
22
+ fail-fast: false
23
+ matrix:
24
+ python-version: ["3.11", "3.12", "3.13"]
25
+ steps:
26
+ - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
27
+ - uses: astral-sh/setup-uv@fac544c07dec837d0ccb6301d7b5580bf5edae39 # v8.2.0
28
+ # The committed lockfile must stay consistent with pyproject.
29
+ - run: uv lock --check
30
+ # search/backlinks/list_tags shell out to ripgrep; install it so those
31
+ # tools and their tests actually run in CI (not silently skipped).
32
+ - run: sudo apt-get update && sudo apt-get install -y ripgrep
33
+ - run: uv venv --python ${{ matrix.python-version }}
34
+ - run: uv pip install -e ".[dev]"
35
+ - run: uv run pytest -q --cov=gateway --cov-report=term-missing
@@ -0,0 +1,34 @@
1
+ name: release
2
+
3
+ # On a vX.Y.Z tag: build, publish a GitHub Release, then publish to PyPI via Trusted
4
+ # Publishing (OIDC, no token). CI (ci.yml) also runs on tags, so a release is only cut
5
+ # from a known-green tree. The GitHub Release runs before PyPI so a not-yet-configured
6
+ # Trusted Publisher cannot block it.
7
+ # One-time setup on pypi.org: add a pending publisher for project `obsidian-gateway` ->
8
+ # owner fszalaj, repo obsidian-gateway, workflow release.yml, environment pypi.
9
+ on:
10
+ push:
11
+ tags: ['v*']
12
+
13
+ permissions:
14
+ contents: write
15
+ id-token: write
16
+
17
+ concurrency:
18
+ group: release-${{ github.ref }}
19
+ cancel-in-progress: false
20
+
21
+ jobs:
22
+ release:
23
+ runs-on: ubuntu-latest
24
+ environment: pypi
25
+ steps:
26
+ - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
27
+ - uses: astral-sh/setup-uv@fac544c07dec837d0ccb6301d7b5580bf5edae39 # v8.2.0
28
+ - run: uv build
29
+ - name: Publish GitHub Release
30
+ env:
31
+ GH_TOKEN: ${{ github.token }}
32
+ run: gh release create "${{ github.ref_name }}" dist/* --title "${{ github.ref_name }}" --generate-notes
33
+ - name: Publish to PyPI (Trusted Publishing)
34
+ uses: pypa/gh-action-pypi-publish@cef221092ed1bacb1cc03d23a2d87d1d172e277b # v1.14.0
@@ -0,0 +1,12 @@
1
+ .venv/
2
+ __pycache__/
3
+ *.egg-info/
4
+ .pytest_cache/
5
+ .coverage
6
+ htmlcov/
7
+ dist/
8
+ build/
9
+
10
+ # Live config - NEVER commit (paths + bearer tokens live only on the host)
11
+ vaults.yaml
12
+ tokens.yaml
@@ -0,0 +1,97 @@
1
+ # Changelog
2
+
3
+ All notable changes to obsidian-gateway. Consumers track the moving **`stable`** branch
4
+ (`uvx --refresh --from git+...@stable`); each release moves `stable` and auto-propagates on
5
+ next launch (no per-repo re-pin). Every release is also an immutable `vX.Y.Z` tag for
6
+ pinning/audit.
7
+
8
+ ## v0.5.1 - 2026-06-16
9
+
10
+ ### Changed
11
+ - **Server instructions**: point agents at `_templates/<type>.md` before creating a page
12
+ (the folder is already reachable via `list_notes`/`read_note` - no new tools).
13
+
14
+ ### Distribution
15
+ - **PyPI Trusted Publishing**: `release.yml` publishes to PyPI on a `vX.Y.Z` tag via OIDC
16
+ (no token). First PyPI release - consumers can `uvx obsidian-gateway` (alongside `@stable`).
17
+ - **MCP Registry**: `server.json` manifest + a `mcp-name` marker in the README, for listing
18
+ in the official MCP Registry.
19
+
20
+ ## v0.5.0 - 2026-06-15
21
+
22
+ ### Features (MCP-FEAT)
23
+ - **Attachments**: `list_attachments` + `read_attachment` - read binary vault files (images
24
+ return as an inline `Image`; PDF/audio/video as a `File`), path-guarded, 25 MiB cap.
25
+ - **Obsidian Canvas**: `list_canvases` + `read_canvas` + `write_canvas` - read/write `.canvas`
26
+ JSON (nodes including `group` nodes, edges, `color` fields), so agents can work with groups
27
+ and colors.
28
+
29
+ ## v0.4.2 - 2026-06-15
30
+
31
+ ### Concurrency (CONC-1)
32
+ - **Per-repo write lock** (`fcntl.flock`): serializes read-modify-write tools
33
+ (`patch_note` / `patch_frontmatter`) and concurrent commits across threads and processes on
34
+ one host, fixing lost-update and mixed-commit races on the shared server. Taken once at the
35
+ tool boundary (the inner commit never re-locks); degrades to a no-op (one-time warning) where
36
+ fcntl/flock is unavailable, rather than failing the write.
37
+ - **Path-scoped commits**: each mutating op commits only its own files (`commit(paths=...)`),
38
+ so a `commit=True` op cannot sweep and mis-attribute a concurrent op's pending change.
39
+ `GIT_LITERAL_PATHSPECS=1` on every git call.
40
+
41
+ ## v0.4.1 - 2026-06-15
42
+
43
+ ### Docs
44
+ - README rewritten (enterprise style; architecture + `stable`-distribution mermaid diagrams; documents the `stable` "update once" model; AI-setup prompt fixed to `@stable` + `--local`).
45
+
46
+ ### Changed
47
+ - The FastMCP server ships an `instructions` prompt describing the tools + Obsidian/git conventions to connecting agents.
48
+ - Reference deploy artifacts for the `@stable` model: `deploy/obsidian-gateway.service` (uv-tool binary) + `deploy/obsidian-gateway-update.{service,timer}` + `deploy/auto-update.sh`.
49
+
50
+ ### Dependencies
51
+ - `ruamel.yaml` allowed up to `<0.20` (lock 0.19.1; Dependabot).
52
+
53
+ ## v0.4.0 - 2026-06-15
54
+
55
+ ### Security
56
+ - Server-mode **error masking**: the HTTP server runs `mask_error_details=True`; the
57
+ gateway's expected client-facing failures surface as `ToolError`, while unexpected OS/git
58
+ errors are hidden from the client.
59
+
60
+ ### Features
61
+ - **`--local` vault auto-detect**: `--local` / `OBSIDIAN_GATEWAY_LOCAL` auto-detects the
62
+ cwd's vault (cwd-is-vault, `./wiki`, a real `*-obsidian-vault`, a child with `.obsidian/`),
63
+ so one global codex/antigravity MCP config works in any repo. Explicit `--vault` still
64
+ wins; a bare invocation still runs the HTTP server.
65
+
66
+ ### Distribution
67
+ - Introduced the moving **`stable`** branch for "update once" rollout (see the header).
68
+
69
+ ## v0.3.0 - 2026-06-15
70
+
71
+ Security, supply-chain, Obsidian-correctness and test coverage. Re-baselines the project
72
+ after the old `v0.2.0` tag was removed; pin this release's commit SHA (or a future PyPI
73
+ `==0.3.0`).
74
+
75
+ ### Security & supply chain
76
+ - Runtime deps bounded to the current major (`fastmcp>=3,<4`, `pyyaml>=6,<7`, `ruamel.yaml>=0.18,<0.19`); `uv.lock` committed and CI-verified (`uv lock --check`).
77
+ - `tokens.yaml` is refused at load time if it is group/world-readable.
78
+ - `atomic_write` preserves an existing note's file mode (a new note is 0644, not mkstemp's 0600).
79
+ - systemd unit hardened with seccomp/capability/rlimit sandboxing (safe for a `--user` unit).
80
+ - CI: Node 24 SHA-pinned actions, Python 3.11-3.13 matrix, ripgrep installed, coverage; Dependabot for github-actions + uv.
81
+
82
+ ### Features & correctness
83
+ - `backlinks` and `rename_note` match the flat note name **case-insensitively** (Obsidian resolves links that way) and accept a trailing `.md` and `^block`.
84
+ - `read_note` rejects a note over 10 MiB; `query_notes` handles a scalar frontmatter `tags:`.
85
+ - `__version__` is sourced from package metadata.
86
+
87
+ ### Tests
88
+ - Coverage 54% -> 82%; added end-to-end tool tests plus rename / gitops / search / security suites.
89
+
90
+ ### Planned
91
+ - PyPI Trusted Publishing, so consumers can `uvx obsidian-gateway==<version>` without a git fetch.
92
+ - Server-mode concurrency hardening (per-note locks, path-scoped commits) and error masking.
93
+
94
+ ## v0.2.0 - removed
95
+
96
+ Initial release: local stdio + HTTP server, 14 git/Obsidian-aware tools, per-vault ACL,
97
+ path guards. This tag was deleted; use v0.3.0 or later.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Filip Szalaj
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,249 @@
1
+ Metadata-Version: 2.4
2
+ Name: obsidian-gateway
3
+ Version: 0.5.1
4
+ Summary: Filesystem/git-native FastMCP gateway serving Obsidian vaults over MCP
5
+ Project-URL: Homepage, https://github.com/fszalaj/obsidian-gateway
6
+ Project-URL: Repository, https://github.com/fszalaj/obsidian-gateway
7
+ Project-URL: Issues, https://github.com/fszalaj/obsidian-gateway/issues
8
+ Author: Filip Szalaj
9
+ License: MIT
10
+ License-File: LICENSE
11
+ Keywords: fastmcp,git,knowledge-base,markdown,mcp,obsidian,vault
12
+ Classifier: Development Status :: 4 - Beta
13
+ Classifier: License :: OSI Approved :: MIT License
14
+ Classifier: Programming Language :: Python :: 3 :: Only
15
+ Classifier: Topic :: Software Development :: Libraries
16
+ Classifier: Topic :: Text Processing :: Markup :: Markdown
17
+ Requires-Python: >=3.11
18
+ Requires-Dist: fastmcp<4,>=3
19
+ Requires-Dist: pyyaml<7,>=6
20
+ Requires-Dist: ruamel-yaml<0.20,>=0.18
21
+ Provides-Extra: dev
22
+ Requires-Dist: pytest-asyncio>=0.24; extra == 'dev'
23
+ Requires-Dist: pytest-cov>=5.0; extra == 'dev'
24
+ Requires-Dist: pytest>=8.0; extra == 'dev'
25
+ Description-Content-Type: text/markdown
26
+
27
+ # obsidian-gateway
28
+
29
+ <!-- mcp-name: io.github.fszalaj/obsidian-gateway -->
30
+
31
+ A filesystem- and git-native **MCP gateway** for Obsidian vaults. AI agents (Claude Code,
32
+ Codex, Cursor, Antigravity) read, search, and **edit** a vault through git-aware,
33
+ Obsidian-aware tools - with **no Obsidian GUI running**, and git as the single source of truth.
34
+
35
+ It exists because the Obsidian *Local REST API* plugin serves only the one vault open in a
36
+ running desktop instance, writes without a lock (silent lost updates), requires a token in
37
+ every client, and treats git as secondary. This gateway operates on the Markdown files
38
+ directly, with git as the system of record.
39
+
40
+ ## Architecture
41
+
42
+ ```mermaid
43
+ flowchart LR
44
+ subgraph clients [Agents]
45
+ A1[Claude Code]
46
+ A2[Codex]
47
+ A3[Antigravity / Cursor]
48
+ end
49
+ A1 --- M(( MCP ))
50
+ A2 --- M
51
+ A3 --- M
52
+ M -->|stdio, per repo, no auth| L[Local gateway]
53
+ M -->|HTTP + bearer + ACL| S[Shared gateway]
54
+ L --> V[/Vault: Markdown files/]
55
+ S --> V
56
+ V <-->|atomic write + scoped commit| G[(git)]
57
+ ```
58
+
59
+ Both modes run the **same** tool implementation over the **same** path guards; they differ in
60
+ transport, authentication/ACL, vault loading, and error masking.
61
+
62
+ ## Two ways to run
63
+
64
+ | | **Local mode** (per repo) | **Shared server** (team) |
65
+ |---|---|---|
66
+ | Use when | a repo wants its own vault for its agents | many people/vaults behind one always-on endpoint |
67
+ | Transport | stdio subprocess (launched by `.mcp.json`) | HTTP (put behind Tailscale/HTTPS) |
68
+ | Secrets / tokens | **none** - nothing to generate | per-user bearer tokens (admin-generated) |
69
+ | Trust boundary | local filesystem access you already have | tailnet + HTTPS + per-vault ACL |
70
+ | Obsidian needed | no | no |
71
+
72
+ Most repos want **Local mode**. The shared server is only for a central, always-on team gateway.
73
+
74
+ ## Distribution - the `stable` branch ("update once")
75
+
76
+ The gateway ships from one moving branch, so a release reaches every consumer and server
77
+ without re-pinning anything by hand.
78
+
79
+ ```mermaid
80
+ flowchart LR
81
+ PR[merge PR to main] --> TAG[tag vX.Y.Z]
82
+ TAG --> MV[move stable -> vX.Y.Z]
83
+ MV --> C["Consumers<br/>uvx --refresh @stable<br/>(updates next session)"]
84
+ MV --> S["Servers<br/>daily uv tool reinstall<br/>(restart if stable moved)"]
85
+ ```
86
+
87
+ - **Consumers** pin `@stable` with `uvx --refresh` -> the ref is re-fetched on every launch, so
88
+ a new release auto-propagates the next time an agent starts. No per-repo re-pin.
89
+ - **Servers** (long-running) run a pinned `uv tool install @stable` plus a daily job that
90
+ reinstalls + restarts only when `stable` actually moves.
91
+ - Every release is **also** an immutable `vX.Y.Z` tag - pin a tag instead of `stable` when you
92
+ need a frozen, auditable version.
93
+
94
+ > A moving *tag* does not work (uvx caches the resolved commit); a *branch* + `--refresh` does.
95
+
96
+ ## Quickstart - local mode (zero secrets)
97
+
98
+ Add this to the repo's `.mcp.json` at the repo root:
99
+
100
+ ```jsonc
101
+ {
102
+ "mcpServers": {
103
+ "wiki": {
104
+ "command": "uvx",
105
+ "args": ["--refresh", "--from", "git+https://github.com/fszalaj/obsidian-gateway@stable",
106
+ "obsidian-gateway", "--local"]
107
+ }
108
+ }
109
+ }
110
+ ```
111
+
112
+ - `--local` auto-detects the vault in the cwd, in order: the cwd itself if it has `.obsidian/`,
113
+ then `./wiki`, then a single `*-obsidian-vault/`, then a single child dir with `.obsidian/`
114
+ (ambiguous matches error). Pass `--vault ./<dir>` to be explicit.
115
+ - `--refresh` re-fetches `@stable` each launch, so releases auto-apply (adds ~1-2s to start).
116
+ - Commits are scoped to the vault's git subdir and attributed to your own
117
+ `git config user.name/email`. No token: the trust boundary is local filesystem access.
118
+
119
+ Open the repo in your agent, approve the `wiki` server once, done.
120
+
121
+ ## Tools
122
+
123
+ | Tool | |
124
+ |---|---|
125
+ | `list_vaults` | vaults reachable here |
126
+ | `list_notes` | Markdown paths in a vault |
127
+ | `read_note` | raw note content |
128
+ | `list_attachments` / `read_attachment` | list / read binary attachments (image -> inline Image, else File) |
129
+ | `list_canvases` / `read_canvas` / `write_canvas` | list / read / write Obsidian Canvas (nodes, groups, colors) |
130
+ | `search` | ripgrep literal/regex full-text |
131
+ | `backlinks` | notes that `[[wikilink]]` to a note |
132
+ | `list_tags` | inline `#tags` with counts |
133
+ | `query_notes` | find notes by frontmatter `type` / `tag` (headless Dataview-lite) |
134
+ | `write_note` | atomic write (+ optional commit) |
135
+ | `patch_note` | insert after a heading or at top/bottom, no full rewrite (+ commit) |
136
+ | `patch_frontmatter` | update YAML frontmatter keys, body intact (+ commit) |
137
+ | `delete_note` | delete a note (+ optional commit) |
138
+ | `rename_note` | rename/move + rewrite inbound flat `[[wikilinks]]` when the name changes (+ optional commit) |
139
+ | `git_status` / `git_commit` | pending changes / commit (subdir-scoped, attributed) |
140
+
141
+ Edits are atomic (temp file + `rename`). Every path goes through `safe_note_path`, which blocks
142
+ traversal, symlink escape, hidden/dotfiles, non-`.md` targets, and `.git`/`.obsidian` - a caller
143
+ can never read or write outside the vault's notes.
144
+
145
+ ## Shared server mode
146
+
147
+ Run this only for a central, always-on gateway reachable over the network.
148
+
149
+ **1. Map vaults** - `cp vaults.example.yaml vaults.yaml`, then set `name -> path / repo_root /
150
+ subdir`. `repo_root` + `subdir` pathspec-scope commits to a vault that lives inside a larger repo.
151
+
152
+ **2. Mint a token per user** (the admin does this):
153
+
154
+ ```bash
155
+ cp tokens.example.yaml tokens.yaml
156
+ openssl rand -hex 32 # once PER user -> the key
157
+ chmod 0600 tokens.yaml # refused at load if group/world-readable
158
+ ```
159
+
160
+ ```yaml
161
+ tokens:
162
+ "8f3c…hex…":
163
+ sub: alice # identity recorded on that user's commits
164
+ vaults: [teamwiki] # the ONLY vaults this token may see/touch
165
+ write: true # false = read-only
166
+ ```
167
+
168
+ A token sees only the vaults in its `vaults` list; anything else returns an opaque
169
+ `vault_forbidden`. `vaults.yaml` + `tokens.yaml` are gitignored.
170
+
171
+ **3. Run** - `uv run obsidian-gateway` (127.0.0.1:8765, path `/mcp/`). For a team box, run it as
172
+ a service behind Tailscale Serve - see `deploy/` and *Operate* below.
173
+
174
+ **4. Connect** - the admin shares the token over a password manager (not chat):
175
+
176
+ ```bash
177
+ claude mcp add --transport http --scope project teamwiki \
178
+ https://YOUR-HOST.<tailnet>.ts.net/mcp/ --header "Authorization: Bearer $GW_TOKEN"
179
+ ```
180
+
181
+ ## Security model
182
+
183
+ - **No secrets in the repo.** `vaults.yaml` / `tokens.yaml` are gitignored; only
184
+ `*.example.yaml` ship. `tokens.yaml` is refused at load if group/world-readable.
185
+ - **Local mode has no credential surface** - a local stdio subprocess; the trust boundary is
186
+ filesystem access the user already has.
187
+ - **Server mode is defense in depth, not a public endpoint** - tailnet ACL + HTTPS + per-user
188
+ `StaticTokenVerifier` bearer token + per-vault ACL. The bearer layer is a shared secret for
189
+ use **behind a trusted tailnet**; do not expose the server publicly.
190
+ - **Path guards on all note I/O** via `safe_note_path` (traversal, symlink, hidden/dotfiles
191
+ incl. `.env`, non-`.md`, `.git`/`.obsidian`). Search/backlinks/tags are bounded to `*.md`.
192
+ - **Server-mode error masking** - the HTTP server runs `mask_error_details=True`: only the
193
+ gateway's own expected failures surface as `ToolError`; unexpected OS/git errors are hidden.
194
+ Local mode keeps details visible.
195
+ - **Commits are attributed** to the requesting user (server) or the local git identity (local),
196
+ and pathspec-scoped to the vault subdir.
197
+
198
+ ## Set it up with an AI
199
+
200
+ Paste this into an agent at a repo's root to wire in local mode:
201
+
202
+ ```
203
+ Add the obsidian-gateway to this repo so agents can read/edit our vault over MCP with zero
204
+ tokens:
205
+ 1. Create or merge `.mcp.json` at the repo root with an mcpServers."wiki" entry that runs:
206
+ uvx --refresh --from git+https://github.com/fszalaj/obsidian-gateway@stable obsidian-gateway --local
207
+ (`--local` auto-detects the vault: ./wiki, a *-obsidian-vault dir, or a dir with .obsidian/.
208
+ If detection is ambiguous, use `--vault ./<vault dir>` instead of `--local`.)
209
+ 2. Verify: `uvx --refresh --from git+https://github.com/fszalaj/obsidian-gateway@stable \
210
+ obsidian-gateway --help` resolves; then in the agent, call list_vaults and read one note.
211
+ Branch + PR, no direct push, no AI attribution.
212
+ ```
213
+
214
+ For the shared server, ask your gateway admin for a token, then run the `claude mcp add …` from
215
+ *Connect* above.
216
+
217
+ ## Operate (servers)
218
+
219
+ A server runs the `@stable` release as a `uv tool`, with a daily job that reinstalls and
220
+ restarts only when `stable` moved. Reference units are in `deploy/`:
221
+
222
+ ```bash
223
+ uv tool install --from git+https://github.com/fszalaj/obsidian-gateway@stable obsidian-gateway
224
+ # the binary lives in the uv cache, so point config at the live files via env:
225
+ # OBSIDIAN_GATEWAY_VAULTS=<dir>/vaults.yaml OBSIDIAN_GATEWAY_TOKENS=<dir>/tokens.yaml
226
+ ```
227
+
228
+ - `deploy/obsidian-gateway.service` - the service (systemd `--user`).
229
+ - `deploy/obsidian-gateway-update.{service,timer}` + `deploy/auto-update.sh` - the daily auto-update.
230
+
231
+ Update now instead of waiting for the timer: `uv tool install --reinstall --from
232
+ git+https://github.com/fszalaj/obsidian-gateway@stable obsidian-gateway`, then restart the
233
+ service. Health: `curl -s -o /dev/null -w '%{http_code}' http://127.0.0.1:8765/mcp/` -> `401`.
234
+
235
+ ## Release (maintainers)
236
+
237
+ 1. PR -> merge to `main` (CI: `uv lock --check`, pytest matrix).
238
+ 2. Bump `pyproject.toml` version + `CHANGELOG.md`.
239
+ 3. Tag `vX.Y.Z` and push the tag (the release workflow builds it).
240
+ 4. Move `stable`: `git branch -f stable vX.Y.Z && git push --force-with-lease origin stable`.
241
+
242
+ Consumers pick it up next session; servers within a day (or restart now).
243
+
244
+ ## Develop
245
+
246
+ ```bash
247
+ uv venv && uv pip install -e ".[dev]"
248
+ uv run pytest # ACL + path guards + edit/frontmatter + detect + masking
249
+ ```