Content
# knowledge-gateway
<!-- mcp-name: io.github.fszalaj/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
```mermaid
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.
```mermaid
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:
```jsonc
{
"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:
```bash
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):
```bash
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
```
```yaml
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):
```bash
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/`:
```bash
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
```bash
uv venv && uv pip install -e ".[dev]"
uv run pytest # ACL + path guards + edit/frontmatter + detect + masking
```
MCP Config
Below is the configuration for this MCP Server. You can copy it directly to Cursor or other MCP clients.
mcp.json
Connection Info
You Might Also Like
No recommendations available