vault-server-MCP
Remote MCP server for Obsidian vault access, giving Claude read/search/archive access to markdown notes via OAuth 2.1 + PKCE auth.
README
MCP server — Obsidian vault access
Remote MCP server (Streamable HTTP transport, OAuth 2.1 + PKCE auth) that gives Claude read/search/archive access to a folder of markdown notes (an Obsidian vault).
Auth is OAuth rather than a plain static token because claude.ai's custom connector UI only has fields for a URL and an OAuth Client ID/Secret — there is no field for a raw bearer token. See "Notes on auth" below for what that means in practice.
Project layout
app/
config.py # .env-driven settings (VAULT_PATH, OAUTH_CLIENT_ID/SECRET, HOST, PORT, LOG_LEVEL, LOG_FILE, PUBLIC_HOSTNAME)
mcp_instance.py # the shared FastMCP instance, wired with the OAuth provider + DNS-rebinding protection
oauth_provider.py # minimal single-tenant OAuth 2.1 authorization server (see below)
audit_log.py # file-based audit logger, wraps every tool call
vault.py # path-safety + filesystem logic (list/search/read/archive)
tools.py # the 5 MCP tools, thin wrappers over vault.py
server.py # builds the ASGI app from the FastMCP instance
main.py # entrypoint: uvicorn.run(app, host=..., port=...)
sample_vault/ # tiny fixture vault for local testing
scripts/manual_test.py # scripted client: runs the OAuth dance, then exercises all 5 tools
deploy/mcp-obsidian.service # example systemd (--user) unit
Tools exposed
read_index()— reads the vault's entry point, configured viaINDEX_PATHin.env(defaultREADME.md). Call this first.list_notes(folder=None)— lists files/folders, excludes_trash/.search_notes(query, limit=10)— full-text search across.mdfiles, excludes_trash/.read_note(path)— returns full file content.create_note(path, content)— creates a new file; fails if one already exists there.edit_note(path, content)— overwrites an existing file's full content; fails if it doesn't exist yet.archive_note(path)— moves a note into_trash/(never deletes physically).
create_note/edit_note can't write directly into _trash/ — that tree is
only ever populated by archive_note.
All path-taking tools reject absolute paths and .. segments (path traversal
protection lives in app/vault.py::resolve_safe_path).
Local setup
python3 -m venv .venv
source .venv/bin/activate
pip install -r requirements.txt
cp .env.example .env
Edit .env:
VAULT_PATH=./sample_vaultfor local testing (a real vault copy also works).OAUTH_CLIENT_ID/OAUTH_CLIENT_SECRET— set real random values, e.g.openssl rand -hex 32for each.- Leave
PUBLIC_HOSTNAMEempty for local-only use.
Run it:
python main.py
It binds to 127.0.0.1:8000 by default (see .env). The MCP endpoint is at
http://127.0.0.1:8000/mcp; OAuth endpoints (/authorize, /token,
/.well-known/oauth-authorization-server, …) live at the same host:port.
Testing before connecting to claude.ai
1. Auth check with curl — hitting the MCP endpoint with no token must
return 401 with a WWW-Authenticate header pointing at the protected
resource metadata (this is what tells claude.ai where to find the OAuth
endpoints):
curl -i http://127.0.0.1:8000/mcp
curl -i http://127.0.0.1:8000/.well-known/oauth-authorization-server
2. Scripted smoke test — runs the full OAuth 2.1 + PKCE flow against
your own server (no browser needed — app/oauth_provider.py auto-approves,
since the real gate is knowing the client secret), then exercises all 5
tools against sample_vault/:
OAUTH_CLIENT_ID=<from .env> OAUTH_CLIENT_SECRET=<from .env> python scripts/manual_test.py
The script creates, edits, and archives a scratch file under
sample_vault/_scratch/ as part of the run, so it's safe to re-run
repeatedly without resetting sample_vault/.
3. MCP Inspector (interactive, closest to how claude.ai will talk to it):
npx @modelcontextprotocol/inspector
In the UI: Transport = Streamable HTTP, URL = http://127.0.0.1:8000/mcp.
Inspector will detect the 401 + metadata and walk you through the OAuth
flow itself, prompting for the Client ID/Secret from your .env.
Connecting to claude.ai
Team/Enterprise plan, as an owner: Admin settings → Connectors → Add custom connector →
- URL:
https://<your-public-hostname>/mcp - Advanced settings → OAuth Client ID: value of
OAUTH_CLIENT_ID - Advanced settings → OAuth Client Secret: value of
OAUTH_CLIENT_SECRET
Each member then goes to Settings → Connectors, finds the connector, and clicks "Connect" — this runs them through the OAuth consent screen (which auto-approves) and gets them their own access token.
Deploying on the VPS (systemd, user-level service)
This runs as a systemctl --user service under your own account — no
dedicated system user or root-owned /opt directory needed. The only root
actions required, ever, are creating /vault (owned by your user) and
enabling "lingering" so the user service can run without an active login
session.
-
One-time, as root (or via
sudo):sudo mkdir -p /vault && sudo chown "$USER":"$USER" /vault sudo loginctl enable-linger "$USER" -
Copy the project to
~/mcp-obsidianon the VPS, create a venv there,pip install -r requirements.txt. -
Create
~/mcp-obsidian/.envwithVAULT_PATH=/vault, strongOAUTH_CLIENT_ID/OAUTH_CLIENT_SECRETvalues,PUBLIC_HOSTNAMEset to the hostname your reverse proxy serves (e.g. a nip.io address or your own domain), andLOG_FILE=~/mcp-obsidian/logs/server.log(expand~to the real home path — systemdEnvironmentFiledoesn't expand~). -
Install the unit file:
mkdir -p ~/.config/systemd/user cp deploy/mcp-obsidian.service ~/.config/systemd/user/ systemctl --user daemon-reload systemctl --user enable --now mcp-obsidian systemctl --user status mcp-obsidian -
Point your reverse proxy (Caddy/nginx) at
127.0.0.1:8000, forwarding theAuthorizationheader through unchanged (this is the default behavior for both — just don't strip it in your config). The proxy also needs to own HTTPS for the exact hostname inPUBLIC_HOSTNAME, since that hostname is baked into the OAuth issuer/resource URLs.
Notes on auth
The server is its own minimal OAuth 2.1 authorization server
(app/oauth_provider.py), not just a resource server checking someone
else's tokens. It registers exactly one pre-shared client — identified by
OAUTH_CLIENT_ID/OAUTH_CLIENT_SECRET from .env — and auto-approves
every /authorize request without a login screen. This is intentional for
a single-company internal tool: the actual security boundary is knowing the
client secret (kept by whoever adds the connector in claude.ai), exactly as
it was with the plain static bearer token this replaced. PKCE, redirect_uri
matching, client-secret verification, and access-token expiry are all
enforced by the mcp SDK itself — oauth_provider.py only stores and
retrieves codes/tokens (in memory; restarting the service invalidates
issued tokens, so anyone connected has to click "Connect" again).
If you ever need real per-user login (rather than one shared credential per
company), swap StaticClientOAuthProvider for a provider that redirects to
a real identity provider (Google Workspace, Microsoft Entra, etc.) in
authorize() — the rest of the server (tools.py, vault.py, the MCP
wiring) doesn't need to change.
Recommended Servers
playwright-mcp
A Model Context Protocol server that enables LLMs to interact with web pages through structured accessibility snapshots without requiring vision models or screenshots.
Magic Component Platform (MCP)
An AI-powered tool that generates modern UI components from natural language descriptions, integrating with popular IDEs to streamline UI development workflow.
Audiense Insights MCP Server
Enables interaction with Audiense Insights accounts via the Model Context Protocol, facilitating the extraction and analysis of marketing insights and audience data including demographics, behavior, and influencer engagement.
VeyraX MCP
Single MCP tool to connect all your favorite tools: Gmail, Calendar and 40 more.
graphlit-mcp-server
The Model Context Protocol (MCP) Server enables integration between MCP clients and the Graphlit service. Ingest anything from Slack to Gmail to podcast feeds, in addition to web crawling, into a Graphlit project - and then retrieve relevant contents from the MCP client.
Kagi MCP Server
An MCP server that integrates Kagi search capabilities with Claude AI, enabling Claude to perform real-time web searches when answering questions that require up-to-date information.
E2B
Using MCP to run code via e2b.
Neon Database
MCP server for interacting with Neon Management API and databases
Exa Search
A Model Context Protocol (MCP) server lets AI assistants like Claude use the Exa AI Search API for web searches. This setup allows AI models to get real-time web information in a safe and controlled way.
Qdrant Server
This repository is an example of how to create a MCP server for Qdrant, a vector search engine.