obsidian-vault-sql-mcp 0.1.0__py3-none-any.whl
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.
- obsidian_mcp/__init__.py +3 -0
- obsidian_mcp/database.py +197 -0
- obsidian_mcp/server.py +177 -0
- obsidian_vault_sql_mcp-0.1.0.dist-info/METADATA +157 -0
- obsidian_vault_sql_mcp-0.1.0.dist-info/RECORD +7 -0
- obsidian_vault_sql_mcp-0.1.0.dist-info/WHEEL +4 -0
- obsidian_vault_sql_mcp-0.1.0.dist-info/entry_points.txt +2 -0
obsidian_mcp/__init__.py
ADDED
obsidian_mcp/database.py
ADDED
|
@@ -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
|
obsidian_mcp/server.py
ADDED
|
@@ -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()
|
|
@@ -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,7 @@
|
|
|
1
|
+
obsidian_mcp/__init__.py,sha256=pJwf0fq2miKGIqARZ-P9844wILdRbmVezxT-mZQ1occ,103
|
|
2
|
+
obsidian_mcp/database.py,sha256=wEA9ZlzWyAiJepLfGjYcX8rWTd9qjAXIq_rQ-2960yQ,7582
|
|
3
|
+
obsidian_mcp/server.py,sha256=YMWLf7l5IcAwDl_iEyTCTNOrTOGp0XLKEvxvehgUNu8,5253
|
|
4
|
+
obsidian_vault_sql_mcp-0.1.0.dist-info/METADATA,sha256=3q15xW2abCXyauY1ksMc9h2jXK2frLWMxL4BhQ_WjD0,3939
|
|
5
|
+
obsidian_vault_sql_mcp-0.1.0.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
|
|
6
|
+
obsidian_vault_sql_mcp-0.1.0.dist-info/entry_points.txt,sha256=GSYex1O3V6cYLsbDWvGpJkpgGzGQ_Y4AluBDsTyDVOA,68
|
|
7
|
+
obsidian_vault_sql_mcp-0.1.0.dist-info/RECORD,,
|