obsidian-vault-sql-mcp 0.1.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.
@@ -0,0 +1,9 @@
1
+ .venv/
2
+ __pycache__/
3
+ *.py[cod]
4
+ *.egg-info/
5
+ dist/
6
+ build/
7
+ .venv/
8
+ .env
9
+ *.db
@@ -0,0 +1,157 @@
1
+ Metadata-Version: 2.4
2
+ Name: obsidian-vault-sql-mcp
3
+ Version: 0.1.0
4
+ Summary: MCP server for Obsidian vaults — file-based note management with SQLite indexing
5
+ License: MIT
6
+ Keywords: markdown,mcp,notes,obsidian,sqlite
7
+ Classifier: License :: OSI Approved :: MIT License
8
+ Classifier: Operating System :: OS Independent
9
+ Classifier: Programming Language :: Python :: 3
10
+ Requires-Python: >=3.11
11
+ Requires-Dist: mcp[cli]>=1.0.0
12
+ Description-Content-Type: text/markdown
13
+
14
+ # obsidian-vault-sql-mcp
15
+
16
+ MCP server for [Obsidian](https://obsidian.md) vaults. Works directly with `.md` files on disk — no Obsidian app needed. Each vault gets its own SQLite index for fast note lookup by name.
17
+
18
+ ## How it works
19
+
20
+ ```
21
+ your-vault/
22
+ ├── notes/
23
+ │ └── my-note.md
24
+ ├── projects/
25
+ │ └── project-alpha.md
26
+ └── data-base-sqlite/ ← created automatically
27
+ └── notes.db ← SQLite index (name → path)
28
+ ```
29
+
30
+ When you call `index_vault`, the server scans all `.md` files and stores their names and paths in SQLite. After that, finding a note by name is instant — no directory traversal needed. When you create, move or delete notes through the server, the index updates automatically.
31
+
32
+ Multiple vaults are supported — each has its own database inside it.
33
+
34
+ ---
35
+
36
+ ## Installation
37
+
38
+ Requires [uv](https://docs.astral.sh/uv/) installed.
39
+
40
+ ```bash
41
+ uvx obsidian-vault-sql-mcp --help
42
+ ```
43
+
44
+ No manual install needed — `uvx` handles everything on first run.
45
+
46
+ ---
47
+
48
+ ## Configuration
49
+
50
+ ### Option A — inline vaults
51
+
52
+ ```bash
53
+ uvx obsidian-vault-sql-mcp --vault personal=/path/to/my-vault --vault work=/path/to/work-vault
54
+ ```
55
+
56
+ ### Option B — config file
57
+
58
+ Create `config.json`:
59
+
60
+ ```json
61
+ {
62
+ "vaults": {
63
+ "personal": "/path/to/my-vault",
64
+ "work": "/path/to/work-vault"
65
+ }
66
+ }
67
+ ```
68
+
69
+ ```bash
70
+ uvx obsidian-vault-sql-mcp --config /path/to/config.json
71
+ ```
72
+
73
+ ---
74
+
75
+ ## Claude Code setup
76
+
77
+ Add to your Claude Code MCP settings (`~/.claude/settings.json`):
78
+
79
+ ```json
80
+ {
81
+ "mcpServers": {
82
+ "obsidian": {
83
+ "command": "uvx",
84
+ "args": [
85
+ "obsidian-vault-sql-mcp",
86
+ "--config", "/path/to/config.json"
87
+ ]
88
+ }
89
+ }
90
+ }
91
+ ```
92
+
93
+ Or with inline vaults:
94
+
95
+ ```json
96
+ {
97
+ "mcpServers": {
98
+ "obsidian": {
99
+ "command": "uvx",
100
+ "args": [
101
+ "obsidian-vault-sql-mcp",
102
+ "--vault", "personal=C:/Users/you/Documents/MyVault"
103
+ ]
104
+ }
105
+ }
106
+ }
107
+ ```
108
+
109
+ ---
110
+
111
+ ## Tools
112
+
113
+ | Tool | Description |
114
+ |------|-------------|
115
+ | `list_vaults()` | List all configured vault names |
116
+ | `index_vault(vault_name)` | Scan vault and build/update SQLite index |
117
+ | `list_notes(vault_name)` | List all indexed notes |
118
+ | `search_notes(vault_name, query)` | Search notes by name (case-insensitive substring) |
119
+ | `read_note(vault_name, note_name)` | Read note content (exact or fuzzy name match) |
120
+ | `create_note(vault_name, name, content, folder?)` | Create a new `.md` note |
121
+ | `update_note(vault_name, note_name, content)` | Overwrite note content |
122
+ | `move_note(vault_name, note_name, new_folder)` | Move note to a different folder |
123
+ | `delete_note(vault_name, note_name)` | Delete note from disk and index |
124
+
125
+ ---
126
+
127
+ ## First-time setup
128
+
129
+ 1. Start the server with your vault path
130
+ 2. Call `index_vault("personal")` once — this scans all existing notes
131
+ 3. Done — all other tools work immediately
132
+
133
+ After that, `index_vault` only needs to be called again if you add/move notes **outside** this server (e.g. manually in Obsidian).
134
+
135
+ ---
136
+
137
+ ## Example usage
138
+
139
+ ```
140
+ search_notes("personal", "yandex")
141
+ → [{ "name": "conspect-yandex-tracker", "relative_path": "notes/conspect-yandex-tracker.md" }]
142
+
143
+ read_note("personal", "conspect-yandex-tracker")
144
+ → { "content": "# Конспект Yandex Tracker\n..." }
145
+
146
+ create_note("personal", "new-idea", "# New Idea\nText here", "inbox")
147
+ → { "path": "inbox/new-idea.md" }
148
+
149
+ move_note("personal", "new-idea", "projects/2026")
150
+ → { "new_path": "projects/2026/new-idea.md" }
151
+ ```
152
+
153
+ ---
154
+
155
+ ## License
156
+
157
+ MIT
@@ -0,0 +1,144 @@
1
+ # obsidian-vault-sql-mcp
2
+
3
+ MCP server for [Obsidian](https://obsidian.md) vaults. Works directly with `.md` files on disk — no Obsidian app needed. Each vault gets its own SQLite index for fast note lookup by name.
4
+
5
+ ## How it works
6
+
7
+ ```
8
+ your-vault/
9
+ ├── notes/
10
+ │ └── my-note.md
11
+ ├── projects/
12
+ │ └── project-alpha.md
13
+ └── data-base-sqlite/ ← created automatically
14
+ └── notes.db ← SQLite index (name → path)
15
+ ```
16
+
17
+ When you call `index_vault`, the server scans all `.md` files and stores their names and paths in SQLite. After that, finding a note by name is instant — no directory traversal needed. When you create, move or delete notes through the server, the index updates automatically.
18
+
19
+ Multiple vaults are supported — each has its own database inside it.
20
+
21
+ ---
22
+
23
+ ## Installation
24
+
25
+ Requires [uv](https://docs.astral.sh/uv/) installed.
26
+
27
+ ```bash
28
+ uvx obsidian-vault-sql-mcp --help
29
+ ```
30
+
31
+ No manual install needed — `uvx` handles everything on first run.
32
+
33
+ ---
34
+
35
+ ## Configuration
36
+
37
+ ### Option A — inline vaults
38
+
39
+ ```bash
40
+ uvx obsidian-vault-sql-mcp --vault personal=/path/to/my-vault --vault work=/path/to/work-vault
41
+ ```
42
+
43
+ ### Option B — config file
44
+
45
+ Create `config.json`:
46
+
47
+ ```json
48
+ {
49
+ "vaults": {
50
+ "personal": "/path/to/my-vault",
51
+ "work": "/path/to/work-vault"
52
+ }
53
+ }
54
+ ```
55
+
56
+ ```bash
57
+ uvx obsidian-vault-sql-mcp --config /path/to/config.json
58
+ ```
59
+
60
+ ---
61
+
62
+ ## Claude Code setup
63
+
64
+ Add to your Claude Code MCP settings (`~/.claude/settings.json`):
65
+
66
+ ```json
67
+ {
68
+ "mcpServers": {
69
+ "obsidian": {
70
+ "command": "uvx",
71
+ "args": [
72
+ "obsidian-vault-sql-mcp",
73
+ "--config", "/path/to/config.json"
74
+ ]
75
+ }
76
+ }
77
+ }
78
+ ```
79
+
80
+ Or with inline vaults:
81
+
82
+ ```json
83
+ {
84
+ "mcpServers": {
85
+ "obsidian": {
86
+ "command": "uvx",
87
+ "args": [
88
+ "obsidian-vault-sql-mcp",
89
+ "--vault", "personal=C:/Users/you/Documents/MyVault"
90
+ ]
91
+ }
92
+ }
93
+ }
94
+ ```
95
+
96
+ ---
97
+
98
+ ## Tools
99
+
100
+ | Tool | Description |
101
+ |------|-------------|
102
+ | `list_vaults()` | List all configured vault names |
103
+ | `index_vault(vault_name)` | Scan vault and build/update SQLite index |
104
+ | `list_notes(vault_name)` | List all indexed notes |
105
+ | `search_notes(vault_name, query)` | Search notes by name (case-insensitive substring) |
106
+ | `read_note(vault_name, note_name)` | Read note content (exact or fuzzy name match) |
107
+ | `create_note(vault_name, name, content, folder?)` | Create a new `.md` note |
108
+ | `update_note(vault_name, note_name, content)` | Overwrite note content |
109
+ | `move_note(vault_name, note_name, new_folder)` | Move note to a different folder |
110
+ | `delete_note(vault_name, note_name)` | Delete note from disk and index |
111
+
112
+ ---
113
+
114
+ ## First-time setup
115
+
116
+ 1. Start the server with your vault path
117
+ 2. Call `index_vault("personal")` once — this scans all existing notes
118
+ 3. Done — all other tools work immediately
119
+
120
+ After that, `index_vault` only needs to be called again if you add/move notes **outside** this server (e.g. manually in Obsidian).
121
+
122
+ ---
123
+
124
+ ## Example usage
125
+
126
+ ```
127
+ search_notes("personal", "yandex")
128
+ → [{ "name": "conspect-yandex-tracker", "relative_path": "notes/conspect-yandex-tracker.md" }]
129
+
130
+ read_note("personal", "conspect-yandex-tracker")
131
+ → { "content": "# Конспект Yandex Tracker\n..." }
132
+
133
+ create_note("personal", "new-idea", "# New Idea\nText here", "inbox")
134
+ → { "path": "inbox/new-idea.md" }
135
+
136
+ move_note("personal", "new-idea", "projects/2026")
137
+ → { "new_path": "projects/2026/new-idea.md" }
138
+ ```
139
+
140
+ ---
141
+
142
+ ## License
143
+
144
+ MIT
@@ -0,0 +1,26 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "obsidian-vault-sql-mcp"
7
+ version = "0.1.0"
8
+ description = "MCP server for Obsidian vaults — file-based note management with SQLite indexing"
9
+ readme = "README.md"
10
+ license = { text = "MIT" }
11
+ requires-python = ">=3.11"
12
+ keywords = ["obsidian", "mcp", "notes", "markdown", "sqlite"]
13
+ classifiers = [
14
+ "Programming Language :: Python :: 3",
15
+ "License :: OSI Approved :: MIT License",
16
+ "Operating System :: OS Independent",
17
+ ]
18
+ dependencies = [
19
+ "mcp[cli]>=1.0.0",
20
+ ]
21
+
22
+ [project.scripts]
23
+ obsidian-vault-sql-mcp = "obsidian_mcp.server:main"
24
+
25
+ [tool.hatch.build.targets.wheel]
26
+ packages = ["src/obsidian_mcp"]
@@ -0,0 +1,3 @@
1
+ """Obsidian MCP Server — file-based vault management with SQLite indexing."""
2
+
3
+ __version__ = "0.1.0"
@@ -0,0 +1,197 @@
1
+ import shutil
2
+ import sqlite3
3
+ from pathlib import Path
4
+ from typing import Optional
5
+
6
+ DB_FOLDER = "data-base-sqlite"
7
+ DB_FILE = "notes.db"
8
+
9
+
10
+ class VaultDB:
11
+ def __init__(self, name: str, vault_root: Path):
12
+ self.name = name
13
+ self.vault_root = vault_root.resolve()
14
+ self.db_dir = self.vault_root / DB_FOLDER
15
+ self.db_path = self.db_dir / DB_FILE
16
+ self.db_dir.mkdir(exist_ok=True)
17
+ self._init_db()
18
+
19
+ def _connect(self) -> sqlite3.Connection:
20
+ conn = sqlite3.connect(str(self.db_path))
21
+ conn.row_factory = sqlite3.Row
22
+ return conn
23
+
24
+ def _init_db(self) -> None:
25
+ with self._connect() as conn:
26
+ conn.execute("""
27
+ CREATE TABLE IF NOT EXISTS notes (
28
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
29
+ name TEXT NOT NULL,
30
+ filename TEXT NOT NULL,
31
+ relative_path TEXT NOT NULL UNIQUE,
32
+ absolute_path TEXT NOT NULL,
33
+ folder TEXT DEFAULT '',
34
+ last_modified REAL,
35
+ created_at REAL DEFAULT (unixepoch())
36
+ )
37
+ """)
38
+ conn.execute("CREATE INDEX IF NOT EXISTS idx_name ON notes(name)")
39
+ conn.commit()
40
+
41
+ # ------------------------------------------------------------------
42
+ # Indexing
43
+ # ------------------------------------------------------------------
44
+
45
+ def index_all(self) -> int:
46
+ """Scan vault root for all .md files and upsert into DB."""
47
+ count = 0
48
+ with self._connect() as conn:
49
+ for md_file in self.vault_root.rglob("*.md"):
50
+ if DB_FOLDER in md_file.parts:
51
+ continue
52
+ rel = md_file.relative_to(self.vault_root)
53
+ folder = str(rel.parent) if str(rel.parent) != "." else ""
54
+ conn.execute(
55
+ """
56
+ INSERT INTO notes (name, filename, relative_path, absolute_path, folder, last_modified)
57
+ VALUES (?, ?, ?, ?, ?, ?)
58
+ ON CONFLICT(relative_path) DO UPDATE SET
59
+ name = excluded.name,
60
+ absolute_path = excluded.absolute_path,
61
+ folder = excluded.folder,
62
+ last_modified = excluded.last_modified
63
+ """,
64
+ (
65
+ md_file.stem,
66
+ md_file.name,
67
+ str(rel).replace("\\", "/"),
68
+ str(md_file),
69
+ folder,
70
+ md_file.stat().st_mtime,
71
+ ),
72
+ )
73
+ count += 1
74
+ conn.commit()
75
+ return count
76
+
77
+ # ------------------------------------------------------------------
78
+ # Search / find
79
+ # ------------------------------------------------------------------
80
+
81
+ def search(self, query: str) -> list[dict]:
82
+ """Case-insensitive substring search by note name."""
83
+ with self._connect() as conn:
84
+ rows = conn.execute(
85
+ "SELECT name, filename, relative_path, folder FROM notes WHERE name LIKE ? ORDER BY name",
86
+ (f"%{query}%",),
87
+ ).fetchall()
88
+ return [dict(r) for r in rows]
89
+
90
+ def find_note(self, note_name: str) -> Optional[dict]:
91
+ """Exact match first, then fuzzy. Returns full row or None."""
92
+ with self._connect() as conn:
93
+ row = conn.execute(
94
+ "SELECT * FROM notes WHERE name = ? LIMIT 1", (note_name,)
95
+ ).fetchone()
96
+ if row is None:
97
+ row = conn.execute(
98
+ "SELECT * FROM notes WHERE name LIKE ? LIMIT 1",
99
+ (f"%{note_name}%",),
100
+ ).fetchone()
101
+ return dict(row) if row else None
102
+
103
+ def list_all(self) -> list[dict]:
104
+ with self._connect() as conn:
105
+ rows = conn.execute(
106
+ "SELECT name, relative_path, folder FROM notes ORDER BY folder, name"
107
+ ).fetchall()
108
+ return [dict(r) for r in rows]
109
+
110
+ # ------------------------------------------------------------------
111
+ # Mutations
112
+ # ------------------------------------------------------------------
113
+
114
+ def create_note(self, name: str, content: str, folder: str = "") -> Path:
115
+ filename = f"{name}.md"
116
+ if folder:
117
+ note_path = self.vault_root / folder / filename
118
+ note_path.parent.mkdir(parents=True, exist_ok=True)
119
+ else:
120
+ note_path = self.vault_root / filename
121
+ note_path.write_text(content, encoding="utf-8")
122
+ rel = note_path.relative_to(self.vault_root)
123
+ with self._connect() as conn:
124
+ conn.execute(
125
+ """
126
+ INSERT INTO notes (name, filename, relative_path, absolute_path, folder, last_modified)
127
+ VALUES (?, ?, ?, ?, ?, ?)
128
+ ON CONFLICT(relative_path) DO UPDATE SET last_modified = excluded.last_modified
129
+ """,
130
+ (
131
+ name,
132
+ filename,
133
+ str(rel).replace("\\", "/"),
134
+ str(note_path),
135
+ folder,
136
+ note_path.stat().st_mtime,
137
+ ),
138
+ )
139
+ conn.commit()
140
+ return rel
141
+
142
+ def update_note(self, note_name: str, content: str) -> None:
143
+ note = self._require(note_name)
144
+ path = Path(note["absolute_path"])
145
+ path.write_text(content, encoding="utf-8")
146
+ with self._connect() as conn:
147
+ conn.execute(
148
+ "UPDATE notes SET last_modified = ? WHERE relative_path = ?",
149
+ (path.stat().st_mtime, note["relative_path"]),
150
+ )
151
+ conn.commit()
152
+
153
+ def move_note(self, note_name: str, new_folder: str) -> Path:
154
+ note = self._require(note_name)
155
+ old_path = Path(note["absolute_path"])
156
+ if new_folder:
157
+ new_dir = self.vault_root / new_folder
158
+ new_dir.mkdir(parents=True, exist_ok=True)
159
+ new_path = new_dir / old_path.name
160
+ else:
161
+ new_path = self.vault_root / old_path.name
162
+ shutil.move(str(old_path), str(new_path))
163
+ new_rel = new_path.relative_to(self.vault_root)
164
+ with self._connect() as conn:
165
+ conn.execute(
166
+ """
167
+ UPDATE notes
168
+ SET relative_path = ?, absolute_path = ?, folder = ?, last_modified = ?
169
+ WHERE relative_path = ?
170
+ """,
171
+ (
172
+ str(new_rel).replace("\\", "/"),
173
+ str(new_path),
174
+ new_folder,
175
+ new_path.stat().st_mtime,
176
+ note["relative_path"],
177
+ ),
178
+ )
179
+ conn.commit()
180
+ return new_rel
181
+
182
+ def delete_note(self, note_name: str) -> None:
183
+ note = self._require(note_name)
184
+ Path(note["absolute_path"]).unlink()
185
+ with self._connect() as conn:
186
+ conn.execute(
187
+ "DELETE FROM notes WHERE relative_path = ?", (note["relative_path"],)
188
+ )
189
+ conn.commit()
190
+
191
+ # ------------------------------------------------------------------
192
+
193
+ def _require(self, note_name: str) -> dict:
194
+ note = self.find_note(note_name)
195
+ if note is None:
196
+ raise ValueError(f"Note '{note_name}' not found in vault '{self.name}'")
197
+ return note
@@ -0,0 +1,177 @@
1
+ import argparse
2
+ import json
3
+ from pathlib import Path
4
+
5
+ from mcp.server.fastmcp import FastMCP
6
+
7
+ from .database import VaultDB
8
+
9
+ mcp = FastMCP("obsidian-mcp")
10
+
11
+ _vaults: dict[str, VaultDB] = {}
12
+
13
+
14
+ def _get(vault_name: str) -> VaultDB:
15
+ if vault_name not in _vaults:
16
+ available = list(_vaults.keys())
17
+ raise ValueError(f"Vault '{vault_name}' not configured. Available: {available}")
18
+ return _vaults[vault_name]
19
+
20
+
21
+ # ------------------------------------------------------------------
22
+ # Tools
23
+ # ------------------------------------------------------------------
24
+
25
+
26
+ @mcp.tool()
27
+ def list_vaults() -> dict:
28
+ """List all configured vaults."""
29
+ return {"vaults": list(_vaults.keys())}
30
+
31
+
32
+ @mcp.tool()
33
+ def index_vault(vault_name: str) -> dict:
34
+ """
35
+ Scan a vault directory and populate its SQLite database with all .md notes.
36
+ Call this once after first setup, or whenever notes are added/moved outside this server.
37
+ """
38
+ db = _get(vault_name)
39
+ count = db.index_all()
40
+ return {"vault": vault_name, "indexed": count}
41
+
42
+
43
+ @mcp.tool()
44
+ def list_notes(vault_name: str) -> dict:
45
+ """List all notes in a vault (reads from SQLite — fast)."""
46
+ db = _get(vault_name)
47
+ return {"vault": vault_name, "notes": db.list_all()}
48
+
49
+
50
+ @mcp.tool()
51
+ def search_notes(vault_name: str, query: str) -> dict:
52
+ """
53
+ Search notes by name in SQLite (case-insensitive substring match).
54
+ Returns list of matching notes with their paths.
55
+ """
56
+ db = _get(vault_name)
57
+ results = db.search(query)
58
+ return {"vault": vault_name, "query": query, "results": results, "count": len(results)}
59
+
60
+
61
+ @mcp.tool()
62
+ def read_note(vault_name: str, note_name: str) -> dict:
63
+ """
64
+ Find note by name via SQLite and return its full markdown content.
65
+ Supports exact and fuzzy name matching.
66
+ """
67
+ db = _get(vault_name)
68
+ note = db.find_note(note_name)
69
+ if note is None:
70
+ raise ValueError(f"Note '{note_name}' not found in vault '{vault_name}'")
71
+ content = Path(note["absolute_path"]).read_text(encoding="utf-8")
72
+ return {
73
+ "vault": vault_name,
74
+ "name": note["name"],
75
+ "path": note["relative_path"],
76
+ "folder": note["folder"],
77
+ "content": content,
78
+ }
79
+
80
+
81
+ @mcp.tool()
82
+ def create_note(vault_name: str, name: str, content: str, folder: str = "") -> dict:
83
+ """
84
+ Create a new .md note and register it in SQLite.
85
+ folder: relative path from vault root, e.g. "projects/2026". Leave empty for root.
86
+ """
87
+ db = _get(vault_name)
88
+ rel = db.create_note(name, content, folder)
89
+ return {"vault": vault_name, "created": name, "path": str(rel).replace("\\", "/")}
90
+
91
+
92
+ @mcp.tool()
93
+ def update_note(vault_name: str, note_name: str, content: str) -> dict:
94
+ """
95
+ Overwrite the content of an existing note.
96
+ SQLite last_modified is updated automatically.
97
+ """
98
+ db = _get(vault_name)
99
+ db.update_note(note_name, content)
100
+ return {"vault": vault_name, "updated": note_name}
101
+
102
+
103
+ @mcp.tool()
104
+ def move_note(vault_name: str, note_name: str, new_folder: str) -> dict:
105
+ """
106
+ Move a note to a different folder inside the vault.
107
+ SQLite path is updated automatically.
108
+ new_folder: relative path from vault root, e.g. "archive/2025". Empty string = vault root.
109
+ """
110
+ db = _get(vault_name)
111
+ new_path = db.move_note(note_name, new_folder)
112
+ return {
113
+ "vault": vault_name,
114
+ "moved": note_name,
115
+ "new_path": str(new_path).replace("\\", "/"),
116
+ }
117
+
118
+
119
+ @mcp.tool()
120
+ def delete_note(vault_name: str, note_name: str) -> dict:
121
+ """Delete a note from disk and remove its record from SQLite."""
122
+ db = _get(vault_name)
123
+ db.delete_note(note_name)
124
+ return {"vault": vault_name, "deleted": note_name}
125
+
126
+
127
+ # ------------------------------------------------------------------
128
+ # Entry point
129
+ # ------------------------------------------------------------------
130
+
131
+
132
+ def main() -> None:
133
+ parser = argparse.ArgumentParser(
134
+ prog="obsidian-mcp",
135
+ description="MCP server for Obsidian vaults with SQLite indexing",
136
+ )
137
+ parser.add_argument(
138
+ "--config",
139
+ metavar="FILE",
140
+ help='Path to JSON config file: {"vaults": {"name": "/path/to/vault"}}',
141
+ )
142
+ parser.add_argument(
143
+ "--vault",
144
+ action="append",
145
+ metavar="NAME=PATH",
146
+ help="Add a vault inline, e.g. --vault personal=/home/user/notes. Can be repeated.",
147
+ )
148
+ args = parser.parse_args()
149
+
150
+ vaults: dict[str, str] = {}
151
+
152
+ if args.config:
153
+ with open(args.config, encoding="utf-8") as f:
154
+ cfg = json.load(f)
155
+ vaults.update(cfg.get("vaults", {}))
156
+
157
+ if args.vault:
158
+ for entry in args.vault:
159
+ if "=" not in entry:
160
+ parser.error(f"--vault must be in NAME=PATH format, got: {entry!r}")
161
+ name, path = entry.split("=", 1)
162
+ vaults[name.strip()] = path.strip()
163
+
164
+ if not vaults:
165
+ parser.error("No vaults configured. Use --config FILE or --vault NAME=PATH.")
166
+
167
+ for name, path in vaults.items():
168
+ vault_path = Path(path)
169
+ if not vault_path.exists():
170
+ raise SystemExit(f"Vault path does not exist: {path}")
171
+ _vaults[name] = VaultDB(name, vault_path)
172
+
173
+ mcp.run()
174
+
175
+
176
+ if __name__ == "__main__":
177
+ main()