Skip to main content

Filesystem/git-native FastMCP knowledge gateway: serve an Obsidian vault over MCP, plus optional code-graph and doc-to-markdown tools

Project description

knowledge-gateway

A single MCP server that gives agents (Claude Code, Codex, Cursor, Gemini, Copilot, Antigravity) three capabilities over one connection:

  • Vault - read, search, and edit a git-backed Markdown/Obsidian vault (no Obsidian GUI), git as the source of truth.
  • Code graph (optional [graph] / [graph-all]) - build and query a code/Ansible knowledge graph of a repo (functions, calls, roles, tasks, handlers, task -> filter edges); AST-only, local, no LLM.
  • Convert (optional [convert]) - turn PDF / Office / image / HTML files into Markdown.

The vault layer exists because the Obsidian Local REST API plugin serves only the one vault open in a running desktop instance, writes without a lock (silent lost updates), needs a token in every client, and treats git as secondary. This gateway operates on the files directly, with git as the system of record - and adds the graph + convert tools the same way: one server, opt-in extras, no new servers to wire.

Architecture

flowchart LR
    subgraph clients [Agents]
        A1[Claude Code]
        A2[Codex]
        A3[Gemini]
        A4[Copilot - VS Code]
        A5[Cursor]
        A6[Antigravity]
    end
    A1 --- M(( MCP ))
    A2 --- M
    A3 --- M
    A4 --- M
    A5 --- M
    A6 --- M
    M -->|stdio, per repo, no auth| L[Local gateway]
    M -->|HTTP + bearer + ACL| S[Shared gateway]
    L --> V[/Vault: Markdown files/]
    S --> V
    V <-->|atomic write + scoped commit| G[(git)]

Both modes run the same tool implementation over the same path guards; they differ in transport, authentication/ACL, vault loading, and error masking.

Two ways to run

Local mode (per repo) Shared server (team)
Use when a repo wants its own vault for its agents many people/vaults behind one always-on endpoint
Transport stdio subprocess (launched by .mcp.json) HTTP (put behind Tailscale/HTTPS)
Secrets / tokens none - nothing to generate per-user bearer tokens (admin-generated)
Trust boundary local filesystem access you already have tailnet + HTTPS + per-vault ACL
Obsidian needed no no

Most repos want Local mode. The shared server is only for a central, always-on team gateway.

Distribution - the stable branch ("update once")

The gateway ships from one moving branch, so a release reaches every consumer and server without re-pinning anything by hand.

flowchart LR
    PR[merge PR to main] --> TAG[tag vX.Y.Z]
    TAG --> MV[move stable -> vX.Y.Z]
    MV --> C["Consumers<br/>uvx --refresh @stable<br/>(updates next session)"]
    MV --> S["Servers<br/>daily uv tool reinstall<br/>(restart if stable moved)"]
  • Consumers pin @stable with uvx --refresh -> the ref is re-fetched on every launch, so a new release auto-propagates the next time an agent starts. No per-repo re-pin.
  • Servers (long-running) run a pinned uv tool install @stable plus a daily job that reinstalls + restarts only when stable actually moves.
  • Every release is also an immutable vX.Y.Z tag - pin a tag instead of stable when you need a frozen, auditable version.

A moving tag does not work (uvx caches the resolved commit); a branch + --refresh does.

Quickstart - local mode (zero secrets)

Add this to the repo's .mcp.json at the repo root:

{
  "mcpServers": {
    "wiki": {
      "command": "uvx",
      "args": ["--refresh", "--from", "git+https://github.com/fszalaj/knowledge-gateway@stable",
               "knowledge-gateway", "--local"]
    }
  }
}
  • --local auto-detects the vault in the cwd, in order: the cwd itself if it has .obsidian/, then ./wiki, then a single *-obsidian-vault/, then a single child dir with .obsidian/ (ambiguous matches error). Pass --vault ./<dir> to be explicit.
  • --refresh re-fetches @stable each launch, so releases auto-apply (adds ~1-2s to start).
  • Commits are scoped to the vault's git subdir and attributed to your own git config user.name/email. No token: the trust boundary is local filesystem access.

Open the repo in your agent, approve the wiki server once, done.

Tools

Tool
list_vaults vaults reachable here
list_notes Markdown paths in a vault
read_note raw note content
list_attachments / read_attachment list / read binary attachments (image -> inline Image, else File)
list_canvases / read_canvas / write_canvas list / read / write Obsidian Canvas (nodes, groups, colors)
search ripgrep literal/regex full-text
backlinks notes that [[wikilink]] to a note
list_tags inline #tags with counts
query_notes find notes by frontmatter type / tag (headless Dataview-lite)
write_note atomic write (+ optional commit)
patch_note insert after a heading or at top/bottom, no full rewrite (+ commit)
patch_frontmatter update YAML frontmatter keys, body intact (+ commit)
delete_note delete a note (+ optional commit)
rename_note rename/move + rewrite inbound flat [[wikilinks]] when the name changes (+ optional commit)
git_status / git_commit pending changes / commit (subdir-scoped, attributed)
list_graphs / graph_query / graph_neighbors / god_nodes / graph_shortest_path / graph_stats query a built code graph (optional [graph])
graph_build build a code/Ansible graph from a source tree into .graph/<name>.json (local mode only)
convert_to_markdown convert a file (PDF/Office/image/HTML/...) in the vault to Markdown (optional [convert])

Edits are atomic (temp file + rename). Every path goes through safe_note_path, which blocks traversal, symlink escape, hidden/dotfiles, non-.md targets, and .git/.obsidian - a caller can never read or write outside the vault's notes.

Code graph and conversion (optional)

Two opt-in capabilities, gated behind extras so the core install stays dependency-free:

Extra Adds
[graph] Python (ast) + Ansible (PyYAML) code graph + the query tools
[graph-all] the above + a broad tree-sitter pass over ~30 languages (JS/TS/TSX, Go, Rust, Java, C#, C/C++, Ruby, PHP, bash, PowerShell, Terraform/HCL, Lua, Kotlin, Swift, Scala, R, Perl, Elixir, Dart, SQL, ...): definitions, imports, and within-file calls
[convert] attachment -> Markdown via markitdown

Build a graph (AST-only - local, no network, no LLM) where the code lives:

knowledge-gateway-graph /path/to/code-repo -o /path/to/vault/.graph/myrepo.json
# in a local-mode session the graph_build tool does the same, writing .graph/<name>.json

Query it over MCP with graph_query / graph_neighbors / god_nodes / graph_shortest_path / graph_stats. The graph captures functions/classes/imports/calls and - uniquely for Ansible - roles, tasks, handlers, include_role/import_tasks/notify, and task -> filter plugin edges. Graph files live in the vault's .graph/, are vault-contained (resolved + checked to stay inside the vault), and are read-only to the gateway - the vault tools never depend on them.

Shared server mode

Run this only for a central, always-on gateway reachable over the network.

1. Map vaults - cp vaults.example.yaml vaults.yaml, then set name -> path / repo_root / subdir. repo_root + subdir pathspec-scope commits to a vault that lives inside a larger repo.

2. Mint a token per user (the admin does this):

cp tokens.example.yaml tokens.yaml
openssl rand -hex 32          # once PER user -> the key
chmod 0600 tokens.yaml        # refused at load if group/world-readable
tokens:
  "8f3c…hex…":
    sub: alice                # identity recorded on that user's commits
    vaults: [teamwiki]        # the ONLY vaults this token may see/touch
    write: true               # false = read-only

A token sees only the vaults in its vaults list; anything else returns an opaque vault_forbidden. vaults.yaml + tokens.yaml are gitignored.

3. Run - uv run knowledge-gateway (127.0.0.1:8765, path /mcp/). For a team box, run it as a service behind Tailscale Serve - see deploy/ and Operate below.

4. Connect - the admin shares the token over a password manager (not chat):

claude mcp add --transport http --scope project teamwiki \
  https://YOUR-HOST.<tailnet>.ts.net/mcp/ --header "Authorization: Bearer $GW_TOKEN"

Security model

  • No secrets in the repo. vaults.yaml / tokens.yaml are gitignored; only *.example.yaml ship. tokens.yaml is refused at load if group/world-readable.
  • Local mode has no credential surface - a local stdio subprocess; the trust boundary is filesystem access the user already has.
  • Server mode is defense in depth, not a public endpoint - tailnet ACL + HTTPS + per-user StaticTokenVerifier bearer token + per-vault ACL. The bearer layer is a shared secret for use behind a trusted tailnet; do not expose the server publicly.
  • Path guards on all note I/O via safe_note_path (traversal, symlink, hidden/dotfiles incl. .env, non-.md, .git/.obsidian). Search/backlinks/tags are bounded to *.md.
  • Server-mode error masking - the HTTP server runs mask_error_details=True: only the gateway's own expected failures surface as ToolError; unexpected OS/git errors are hidden. Local mode keeps details visible.
  • Commits are attributed to the requesting user (server) or the local git identity (local), and pathspec-scoped to the vault subdir.

Set it up with an AI

Paste this into an agent at a repo's root to wire in local mode:

Add the knowledge-gateway to this repo so agents can read/edit our vault over MCP with zero
tokens:
1. Create or merge `.mcp.json` at the repo root with an mcpServers."wiki" entry that runs:
   uvx --refresh --from git+https://github.com/fszalaj/knowledge-gateway@stable knowledge-gateway --local
   (`--local` auto-detects the vault: ./wiki, a *-obsidian-vault dir, or a dir with .obsidian/.
   If detection is ambiguous, use `--vault ./<vault dir>` instead of `--local`.)
2. Verify: `uvx --refresh --from git+https://github.com/fszalaj/knowledge-gateway@stable \
   knowledge-gateway --help` resolves; then in the agent, call list_vaults and read one note.
Branch + PR, no direct push, no AI attribution.

For the shared server, ask your gateway admin for a token, then run the claude mcp add … from Connect above.

Operate (servers)

A server runs the @stable release as a uv tool, with a daily job that reinstalls and restarts only when stable moved. Reference units are in deploy/:

uv tool install --from git+https://github.com/fszalaj/knowledge-gateway@stable knowledge-gateway
# the binary lives in the uv cache, so point config at the live files via env:
#   KNOWLEDGE_GATEWAY_VAULTS=<dir>/vaults.yaml   KNOWLEDGE_GATEWAY_TOKENS=<dir>/tokens.yaml
  • deploy/knowledge-gateway.service - the service (systemd --user).
  • deploy/knowledge-gateway-update.{service,timer} + deploy/auto-update.sh - the daily auto-update.

Update now instead of waiting for the timer: uv tool install --reinstall --from git+https://github.com/fszalaj/knowledge-gateway@stable knowledge-gateway, then restart the service. Health: curl -s -o /dev/null -w '%{http_code}' http://127.0.0.1:8765/mcp/ -> 401.

Release (maintainers)

  1. PR -> merge to main (CI: uv lock --check, pytest matrix).
  2. Bump pyproject.toml version + CHANGELOG.md.
  3. Tag vX.Y.Z and push the tag (the release workflow builds it).
  4. Move stable: git branch -f stable vX.Y.Z && git push --force-with-lease origin stable.

Consumers pick it up next session; servers within a day (or restart now).

Develop

uv venv && uv pip install -e ".[dev]"
uv run pytest                              # ACL + path guards + edit/frontmatter + detect + masking

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

knowledge_gateway-0.8.2.tar.gz (173.3 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

knowledge_gateway-0.8.2-py3-none-any.whl (45.4 kB view details)

Uploaded Python 3

File details

Details for the file knowledge_gateway-0.8.2.tar.gz.

File metadata

  • Download URL: knowledge_gateway-0.8.2.tar.gz
  • Upload date:
  • Size: 173.3 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.13

File hashes

Hashes for knowledge_gateway-0.8.2.tar.gz
Algorithm Hash digest
SHA256 6cd592facbd070e463da3ef9e3d6fea015093a9dbc697c6e87574699e2ffe7e3
MD5 bb84bfbfed52ebf054b44e8e531fb8d1
BLAKE2b-256 20c17aac696d372e5dfcc5d3c770927280031c12fadce4e3f8afd56131891e86

See more details on using hashes here.

Provenance

The following attestation bundles were made for knowledge_gateway-0.8.2.tar.gz:

Publisher: release.yml on fszalaj/knowledge-gateway

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file knowledge_gateway-0.8.2-py3-none-any.whl.

File metadata

File hashes

Hashes for knowledge_gateway-0.8.2-py3-none-any.whl
Algorithm Hash digest
SHA256 44e526926893d3aab29af5e37fbc323087db2d989da1f88aa053ff495abed7aa
MD5 975f65947de1352c07b5f00c04dc5797
BLAKE2b-256 ce565ecf80fd8d793e66c51e21bea3f48915cc15bfeacd948bf3cd33130dbc05

See more details on using hashes here.

Provenance

The following attestation bundles were made for knowledge_gateway-0.8.2-py3-none-any.whl:

Publisher: release.yml on fszalaj/knowledge-gateway

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page