obsidian-remote-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.
- obsidian_remote_mcp-0.1.0/.env.example +20 -0
- obsidian_remote_mcp-0.1.0/.github/workflows/ci.yml +28 -0
- obsidian_remote_mcp-0.1.0/.github/workflows/release.yml +64 -0
- obsidian_remote_mcp-0.1.0/.gitignore +24 -0
- obsidian_remote_mcp-0.1.0/Dockerfile +11 -0
- obsidian_remote_mcp-0.1.0/FEATURES.md +254 -0
- obsidian_remote_mcp-0.1.0/LICENSE +21 -0
- obsidian_remote_mcp-0.1.0/PKG-INFO +246 -0
- obsidian_remote_mcp-0.1.0/README.md +229 -0
- obsidian_remote_mcp-0.1.0/docker-compose.yml +17 -0
- obsidian_remote_mcp-0.1.0/pyproject.toml +52 -0
- obsidian_remote_mcp-0.1.0/src/obsidian_mcp/__init__.py +1 -0
- obsidian_remote_mcp-0.1.0/src/obsidian_mcp/config.py +51 -0
- obsidian_remote_mcp-0.1.0/src/obsidian_mcp/domain/__init__.py +0 -0
- obsidian_remote_mcp-0.1.0/src/obsidian_mcp/domain/index.py +198 -0
- obsidian_remote_mcp-0.1.0/src/obsidian_mcp/domain/models.py +47 -0
- obsidian_remote_mcp-0.1.0/src/obsidian_mcp/domain/parser.py +132 -0
- obsidian_remote_mcp-0.1.0/src/obsidian_mcp/server.py +715 -0
- obsidian_remote_mcp-0.1.0/src/obsidian_mcp/storage/__init__.py +0 -0
- obsidian_remote_mcp-0.1.0/src/obsidian_mcp/storage/filesystem.py +51 -0
- obsidian_remote_mcp-0.1.0/src/obsidian_mcp/storage/locking.py +16 -0
- obsidian_remote_mcp-0.1.0/src/obsidian_mcp/storage/watcher.py +96 -0
- obsidian_remote_mcp-0.1.0/src/obsidian_mcp/tools/__init__.py +0 -0
- obsidian_remote_mcp-0.1.0/src/obsidian_mcp/tools/attachments.py +94 -0
- obsidian_remote_mcp-0.1.0/src/obsidian_mcp/tools/canvas.py +193 -0
- obsidian_remote_mcp-0.1.0/src/obsidian_mcp/tools/folders.py +153 -0
- obsidian_remote_mcp-0.1.0/src/obsidian_mcp/tools/kanban.py +285 -0
- obsidian_remote_mcp-0.1.0/src/obsidian_mcp/tools/query.py +318 -0
- obsidian_remote_mcp-0.1.0/src/obsidian_mcp/tools/read.py +263 -0
- obsidian_remote_mcp-0.1.0/src/obsidian_mcp/tools/templates.py +94 -0
- obsidian_remote_mcp-0.1.0/src/obsidian_mcp/tools/write.py +384 -0
- obsidian_remote_mcp-0.1.0/tests/conftest.py +33 -0
- obsidian_remote_mcp-0.1.0/tests/fixtures/sample_vault/broken_yaml.md +6 -0
- obsidian_remote_mcp-0.1.0/tests/fixtures/sample_vault/headings.md +11 -0
- obsidian_remote_mcp-0.1.0/tests/fixtures/sample_vault/no_frontmatter.md +3 -0
- obsidian_remote_mcp-0.1.0/tests/fixtures/sample_vault/simple.md +8 -0
- obsidian_remote_mcp-0.1.0/tests/fixtures/sample_vault/with_blocks.md +17 -0
- obsidian_remote_mcp-0.1.0/tests/fixtures/sample_vault/with_callouts.md +23 -0
- obsidian_remote_mcp-0.1.0/tests/fixtures/sample_vault/with_tasks.md +17 -0
- obsidian_remote_mcp-0.1.0/tests/test_coverage_gaps.py +228 -0
- obsidian_remote_mcp-0.1.0/tests/test_index.py +68 -0
- obsidian_remote_mcp-0.1.0/tests/test_index_extended.py +103 -0
- obsidian_remote_mcp-0.1.0/tests/test_parser.py +67 -0
- obsidian_remote_mcp-0.1.0/tests/test_parser_extended.py +150 -0
- obsidian_remote_mcp-0.1.0/tests/test_phase10.py +222 -0
- obsidian_remote_mcp-0.1.0/tests/test_phase5.py +165 -0
- obsidian_remote_mcp-0.1.0/tests/test_phase6.py +213 -0
- obsidian_remote_mcp-0.1.0/tests/test_phase7.py +203 -0
- obsidian_remote_mcp-0.1.0/tests/test_phase8.py +210 -0
- obsidian_remote_mcp-0.1.0/tests/test_phase9.py +238 -0
- obsidian_remote_mcp-0.1.0/tests/test_storage.py +55 -0
- obsidian_remote_mcp-0.1.0/tests/test_tools_query.py +183 -0
- obsidian_remote_mcp-0.1.0/tests/test_tools_write.py +56 -0
- obsidian_remote_mcp-0.1.0/uv.lock +1450 -0
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# Required: absolute path to your Obsidian vault
|
|
2
|
+
VAULT_PATH=/path/to/your/vault
|
|
3
|
+
|
|
4
|
+
# Set to true to disable all write operations
|
|
5
|
+
READ_ONLY=false
|
|
6
|
+
|
|
7
|
+
# Comma-separated relative paths where writes are allowed (empty = everywhere)
|
|
8
|
+
WRITE_PATHS=
|
|
9
|
+
|
|
10
|
+
# Comma-separated paths to exclude from listing/search
|
|
11
|
+
EXCLUDE_PATHS=private,.obsidian,.trash
|
|
12
|
+
|
|
13
|
+
# Transport mode: "stdio" (default, local use) or "sse" (remote/network use)
|
|
14
|
+
TRANSPORT=stdio
|
|
15
|
+
|
|
16
|
+
# API key for network transport (SSE). If set, all requests must include:
|
|
17
|
+
# Authorization: Bearer <your-key>
|
|
18
|
+
# Generate with: openssl rand -hex 32
|
|
19
|
+
# Not required for stdio transport.
|
|
20
|
+
API_KEY=
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main]
|
|
6
|
+
pull_request:
|
|
7
|
+
branches: [main]
|
|
8
|
+
|
|
9
|
+
jobs:
|
|
10
|
+
ci:
|
|
11
|
+
runs-on: ubuntu-latest
|
|
12
|
+
|
|
13
|
+
steps:
|
|
14
|
+
- uses: actions/checkout@v4
|
|
15
|
+
|
|
16
|
+
- name: Set up uv
|
|
17
|
+
uses: astral-sh/setup-uv@v4
|
|
18
|
+
with:
|
|
19
|
+
python-version: "3.12"
|
|
20
|
+
|
|
21
|
+
- name: Install dependencies
|
|
22
|
+
run: uv sync --dev
|
|
23
|
+
|
|
24
|
+
- name: Lint (ruff)
|
|
25
|
+
run: uv run ruff check src/ tests/
|
|
26
|
+
|
|
27
|
+
- name: Tests
|
|
28
|
+
run: uv run pytest tests/ -v --tb=short
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
name: Release
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
tags: ['v*']
|
|
6
|
+
|
|
7
|
+
jobs:
|
|
8
|
+
# ── PyPI ────────────────────────────────────────────────────────────────────
|
|
9
|
+
publish-pypi:
|
|
10
|
+
name: Publish to PyPI
|
|
11
|
+
runs-on: ubuntu-latest
|
|
12
|
+
permissions:
|
|
13
|
+
id-token: write # required for trusted publishing
|
|
14
|
+
|
|
15
|
+
steps:
|
|
16
|
+
- uses: actions/checkout@v4
|
|
17
|
+
|
|
18
|
+
- uses: astral-sh/setup-uv@v4
|
|
19
|
+
with:
|
|
20
|
+
python-version: "3.12"
|
|
21
|
+
|
|
22
|
+
- name: Build
|
|
23
|
+
run: uv build
|
|
24
|
+
|
|
25
|
+
- name: Publish
|
|
26
|
+
run: uv publish
|
|
27
|
+
env:
|
|
28
|
+
UV_PUBLISH_TOKEN: ${{ secrets.PYPI_TOKEN }}
|
|
29
|
+
|
|
30
|
+
# ── Docker → GHCR ───────────────────────────────────────────────────────────
|
|
31
|
+
publish-docker:
|
|
32
|
+
name: Publish Docker image to GHCR
|
|
33
|
+
runs-on: ubuntu-latest
|
|
34
|
+
permissions:
|
|
35
|
+
contents: read
|
|
36
|
+
packages: write
|
|
37
|
+
|
|
38
|
+
steps:
|
|
39
|
+
- uses: actions/checkout@v4
|
|
40
|
+
|
|
41
|
+
- name: Log in to GHCR
|
|
42
|
+
uses: docker/login-action@v3
|
|
43
|
+
with:
|
|
44
|
+
registry: ghcr.io
|
|
45
|
+
username: ${{ github.actor }}
|
|
46
|
+
password: ${{ secrets.GITHUB_TOKEN }}
|
|
47
|
+
|
|
48
|
+
- name: Extract metadata
|
|
49
|
+
id: meta
|
|
50
|
+
uses: docker/metadata-action@v5
|
|
51
|
+
with:
|
|
52
|
+
images: ghcr.io/ykoellmann/obsidian-mcp
|
|
53
|
+
tags: |
|
|
54
|
+
type=semver,pattern={{version}}
|
|
55
|
+
type=semver,pattern={{major}}.{{minor}}
|
|
56
|
+
type=raw,value=latest
|
|
57
|
+
|
|
58
|
+
- name: Build and push
|
|
59
|
+
uses: docker/build-push-action@v6
|
|
60
|
+
with:
|
|
61
|
+
context: .
|
|
62
|
+
push: true
|
|
63
|
+
tags: ${{ steps.meta.outputs.tags }}
|
|
64
|
+
labels: ${{ steps.meta.outputs.labels }}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# Python
|
|
2
|
+
.venv/
|
|
3
|
+
__pycache__/
|
|
4
|
+
*.pyc
|
|
5
|
+
*.pyo
|
|
6
|
+
dist/
|
|
7
|
+
*.egg-info/
|
|
8
|
+
|
|
9
|
+
# Caches
|
|
10
|
+
.pytest_cache/
|
|
11
|
+
.ruff_cache/
|
|
12
|
+
.mypy_cache/
|
|
13
|
+
|
|
14
|
+
# Environment
|
|
15
|
+
.env
|
|
16
|
+
|
|
17
|
+
# Local planning notes
|
|
18
|
+
plan.md
|
|
19
|
+
|
|
20
|
+
# Example vault (local only – jeder nutzt seinen eigenen Vault)
|
|
21
|
+
example-vault/
|
|
22
|
+
|
|
23
|
+
# Lock files for file operations (vault per-file locks)
|
|
24
|
+
*.md.lock
|
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
# Feature-Roadmap – second-brain-mcp
|
|
2
|
+
|
|
3
|
+
Ziel: vollständige Obsidian-Unterstützung und darüber hinaus.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Phase 2 – Enhanced Core (nächste Schritte)
|
|
8
|
+
|
|
9
|
+
### 2.1 Search mit Snippets & Ranking
|
|
10
|
+
**Tool:** `search_notes_tool` erweitern
|
|
11
|
+
|
|
12
|
+
Aktuell: gibt nur Pfad + Tags zurück, kein Kontext.
|
|
13
|
+
|
|
14
|
+
```python
|
|
15
|
+
# Neu: strukturierte Ergebnisse
|
|
16
|
+
{
|
|
17
|
+
"path": "Notizen/Python-Tipps.md",
|
|
18
|
+
"score": 0.92,
|
|
19
|
+
"snippets": [
|
|
20
|
+
{"line": 23, "text": "...`filelock` verhält sich anders wenn der **Lock-File** auf einem NFS-Mount..."}
|
|
21
|
+
],
|
|
22
|
+
"tags": ["konzept/programmierung"]
|
|
23
|
+
}
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
- Ranking: exakter Treffer > Wort-Anfang > Teilstring
|
|
27
|
+
- Snippets: ±2 Zeilen um den Treffer, Match bold-markiert
|
|
28
|
+
- Optional: `regex=True` für Regex-Suche
|
|
29
|
+
- Optional: `limit=20` für Ergebnis-Anzahl
|
|
30
|
+
|
|
31
|
+
### 2.2 `move_note` – Umbenennen + Backlink-Rewriting
|
|
32
|
+
**Neues Tool**
|
|
33
|
+
|
|
34
|
+
```python
|
|
35
|
+
@mcp.tool()
|
|
36
|
+
def move_note(from_path: str, to_path: str) -> dict:
|
|
37
|
+
# 1. Datei verschieben (atomar)
|
|
38
|
+
# 2. Alle .md-Dateien im Vault scannen
|
|
39
|
+
# 3. [[from_path]]-Wikilinks auf [[to_path]] umschreiben
|
|
40
|
+
# 4. Index aktualisieren
|
|
41
|
+
# Gibt zurück: {moved: ..., updated_links_in: [...]}
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
### 2.3 `get_orphans` – Notizen ohne Backlinks
|
|
45
|
+
**Neues Tool**
|
|
46
|
+
|
|
47
|
+
```python
|
|
48
|
+
@mcp.tool()
|
|
49
|
+
def get_orphans(exclude_folders: list[str] = ["Journal", "Templates"]) -> list[str]:
|
|
50
|
+
# Notizen die von keiner anderen Notiz verlinkt werden
|
|
51
|
+
# Journal-Einträge standardmäßig ausgeschlossen
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
### 2.4 `get_broken_links` – tote Wikilinks finden
|
|
55
|
+
**Neues Tool**
|
|
56
|
+
|
|
57
|
+
```python
|
|
58
|
+
@mcp.tool()
|
|
59
|
+
def get_broken_links() -> list[dict]:
|
|
60
|
+
# [{source: "Notizen/X.md", link: "[[NichtExistent]]", line: 12}]
|
|
61
|
+
# Prüft gegen: existierende Dateien + bekannte Aliases
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
### 2.5 `list_notes_with_meta` – Listing mit Metadaten
|
|
65
|
+
**`list_notes_tool` Erweiterung**
|
|
66
|
+
|
|
67
|
+
```python
|
|
68
|
+
@mcp.tool()
|
|
69
|
+
def list_notes_tool(folder: str = "", include_meta: bool = False) -> list[str | dict]:
|
|
70
|
+
# include_meta=True: [{path, title, tags, status, created, mtime}]
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
---
|
|
74
|
+
|
|
75
|
+
## Phase 3 – Graph & Navigation
|
|
76
|
+
|
|
77
|
+
### 3.1 `get_link_graph` – traversierbarer Link-Graph
|
|
78
|
+
**Neues Tool – das wichtigste dieser Phase**
|
|
79
|
+
|
|
80
|
+
```python
|
|
81
|
+
@mcp.tool()
|
|
82
|
+
def get_link_graph(
|
|
83
|
+
root: str, # Startnotiz
|
|
84
|
+
depth: int = 2, # wie viele Ebenen ausgehend
|
|
85
|
+
direction: str = "both", # "outgoing" | "incoming" | "both"
|
|
86
|
+
include_tags: bool = False
|
|
87
|
+
) -> dict:
|
|
88
|
+
# {
|
|
89
|
+
# "root": "Projekte/second-brain-mcp.md",
|
|
90
|
+
# "nodes": [{"path": ..., "title": ..., "tags": [...]}],
|
|
91
|
+
# "edges": [{"from": ..., "to": ..., "type": "wikilink"}]
|
|
92
|
+
# }
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
Erlaubt: „Zeig mir alles was mit diesem Projekt zusammenhängt (2 Ebenen tief)".
|
|
96
|
+
|
|
97
|
+
### 3.2 `get_tag_tree` – verschachtelter Tag-Baum
|
|
98
|
+
**Neues Tool**
|
|
99
|
+
|
|
100
|
+
```python
|
|
101
|
+
@mcp.tool()
|
|
102
|
+
def get_tag_tree() -> dict:
|
|
103
|
+
# {
|
|
104
|
+
# "konzept": {
|
|
105
|
+
# "mcp": ["Notizen/MCP-Protokoll.md"],
|
|
106
|
+
# "ki": {
|
|
107
|
+
# "llm": ["Notizen/Claude.md"]
|
|
108
|
+
# }
|
|
109
|
+
# },
|
|
110
|
+
# "projekt": {
|
|
111
|
+
# "aktiv": ["Projekte/second-brain-mcp.md"]
|
|
112
|
+
# }
|
|
113
|
+
# }
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
### 3.3 `get_vault_stats` – Vault-Statistiken
|
|
117
|
+
**Neues Tool**
|
|
118
|
+
|
|
119
|
+
```python
|
|
120
|
+
@mcp.tool()
|
|
121
|
+
def get_vault_stats() -> dict:
|
|
122
|
+
# {total_notes, total_links, total_tags, orphans_count,
|
|
123
|
+
# broken_links_count, most_linked: [...], most_linking: [...]}
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
---
|
|
127
|
+
|
|
128
|
+
## Phase 4 – Vollständige Obsidian-Feature-Unterstützung
|
|
129
|
+
|
|
130
|
+
### 4.1 Block-Referenzen
|
|
131
|
+
**Parser + Index erweitern**
|
|
132
|
+
|
|
133
|
+
- Parsen von `^block-id`-Ankern in Notizen
|
|
134
|
+
- Auflösen von `[[Note^block-id]]`-Links
|
|
135
|
+
- Neues Tool: `read_block(path, block_id) -> str`
|
|
136
|
+
- Index: `block_index[note_path][block_id] = line_number`
|
|
137
|
+
|
|
138
|
+
### 4.2 Aliases – transparente Auflösung
|
|
139
|
+
**Parser + Index erweitern**
|
|
140
|
+
|
|
141
|
+
- `aliases:` aus Frontmatter in Index aufnehmen
|
|
142
|
+
- `[[Alias]]` → automatisch zur echten Notiz auflösen
|
|
143
|
+
- `get_broken_links` berücksichtigt Aliases (kein false positive)
|
|
144
|
+
- Neues Tool: `resolve_alias(name) -> str | None`
|
|
145
|
+
|
|
146
|
+
### 4.3 Embedded Notes – `![[Note]]` Transclusion
|
|
147
|
+
**Parser + neues Tool**
|
|
148
|
+
|
|
149
|
+
- `![[Note]]` und `![[Note#Section]]` parsen
|
|
150
|
+
- Neues Tool: `render_note(path) -> str` – löst alle Embeddings auf,
|
|
151
|
+
gibt vollständigen Text zurück (für vollständigen Kontext an Claude)
|
|
152
|
+
|
|
153
|
+
### 4.4 Daily Notes
|
|
154
|
+
**Neues Tool**
|
|
155
|
+
|
|
156
|
+
```python
|
|
157
|
+
@mcp.tool()
|
|
158
|
+
def get_daily_note(date: str = "today") -> dict:
|
|
159
|
+
# date: "today" | "yesterday" | "YYYY-MM-DD"
|
|
160
|
+
# Sucht in Journal/ nach YYYY-MM-DD.md
|
|
161
|
+
# Erstellt aus Template wenn nicht vorhanden
|
|
162
|
+
|
|
163
|
+
@mcp.tool()
|
|
164
|
+
def get_daily_notes_range(from_date: str, to_date: str) -> list[dict]:
|
|
165
|
+
# Alle Daily Notes im Zeitraum, mit Aufgaben-Summary
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
### 4.5 Task-Extraktion quer über den Vault
|
|
169
|
+
**Neues Tool**
|
|
170
|
+
|
|
171
|
+
```python
|
|
172
|
+
@mcp.tool()
|
|
173
|
+
def get_tasks(
|
|
174
|
+
status: str = "open", # "open" | "done" | "all"
|
|
175
|
+
folder: str = "",
|
|
176
|
+
tag: str | None = None
|
|
177
|
+
) -> list[dict]:
|
|
178
|
+
# [{text, done, source_path, line, due_date (aus [[due::YYYY-MM-DD]])}]
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
### 4.6 Callout-Parsing
|
|
182
|
+
**Parser erweitern**
|
|
183
|
+
|
|
184
|
+
- Callout-Typen erkennen: `[!NOTE]`, `[!WARNING]`, `[!TIP]`, `[!IMPORTANT]`, `[!QUESTION]`
|
|
185
|
+
- In `Note`-Dataclass aufnehmen: `callouts: list[Callout]`
|
|
186
|
+
- `read_note` gibt Callouts strukturiert zurück
|
|
187
|
+
|
|
188
|
+
### 4.7 Canvas-Support (`.canvas`-Dateien)
|
|
189
|
+
**Neues Tool**
|
|
190
|
+
|
|
191
|
+
Obsidian Canvas = JSON-Format mit Nodes und Edges.
|
|
192
|
+
|
|
193
|
+
```python
|
|
194
|
+
@mcp.tool()
|
|
195
|
+
def read_canvas(path: str) -> dict:
|
|
196
|
+
# {nodes: [{id, type, text/file, x, y}], edges: [{from, to, label}]}
|
|
197
|
+
|
|
198
|
+
@mcp.tool()
|
|
199
|
+
def list_canvases() -> list[str]:
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
---
|
|
203
|
+
|
|
204
|
+
## Phase 5 – Erweiterte Suche & Abfragen
|
|
205
|
+
|
|
206
|
+
### 5.1 Dataview-ähnliche Abfragen
|
|
207
|
+
**Neues Tool**
|
|
208
|
+
|
|
209
|
+
```python
|
|
210
|
+
@mcp.tool()
|
|
211
|
+
def query_notes(
|
|
212
|
+
tags: list[str] | None = None,
|
|
213
|
+
status: str | None = None,
|
|
214
|
+
frontmatter_filter: dict | None = None, # {"prioritaet": {"gte": 3}}
|
|
215
|
+
sort_by: str = "mtime",
|
|
216
|
+
limit: int = 50
|
|
217
|
+
) -> list[dict]:
|
|
218
|
+
# Filtert Notizen nach Metadaten ohne Volltextsuche
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
### 5.2 Fuzzy- und Regex-Suche
|
|
222
|
+
**`search_notes_tool` erweitern**
|
|
223
|
+
|
|
224
|
+
- `mode: "exact" | "fuzzy" | "regex"`
|
|
225
|
+
- Fuzzy: Levenshtein-Distanz, für Tippfehler-Toleranz
|
|
226
|
+
|
|
227
|
+
### 5.3 Template-Engine
|
|
228
|
+
**Neues Tool**
|
|
229
|
+
|
|
230
|
+
```python
|
|
231
|
+
@mcp.tool()
|
|
232
|
+
def create_from_template(
|
|
233
|
+
template_path: str,
|
|
234
|
+
output_path: str,
|
|
235
|
+
variables: dict # {"title": "Mein Projekt", "date": "2026-07-23"}
|
|
236
|
+
) -> dict:
|
|
237
|
+
# Ersetzt {{title}}, {{date}} etc. im Template
|
|
238
|
+
# Erstellt Zieldatei atomar
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
---
|
|
242
|
+
|
|
243
|
+
## Build-Order
|
|
244
|
+
|
|
245
|
+
```
|
|
246
|
+
Phase 2: 2.4 get_broken_links → 2.3 get_orphans → 2.1 Search+Snippets → 2.2 move_note → 2.5 list_meta
|
|
247
|
+
Phase 3: 4.2 Aliases (braucht Index) → 3.1 get_link_graph → 3.2 get_tag_tree → 3.3 vault_stats
|
|
248
|
+
Phase 4: 4.1 Block-Refs → 4.3 Embeddings → 4.4 Daily Notes → 4.5 Tasks → 4.6 Callouts → 4.7 Canvas
|
|
249
|
+
Phase 5: 5.1 Query → 5.2 Fuzzy → 5.3 Templates
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
Aliases (4.2) vor dem Link-Graph (3.1), weil der Graph Aliases für korrekte
|
|
253
|
+
Auflösung braucht. Block-Refs (4.1) vor Embeddings (4.3), weil Embeddings
|
|
254
|
+
Block-Refs auflösen müssen.
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Yannik Koellmann
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: obsidian-remote-mcp
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: MCP server for Obsidian vaults — read, write, search, graph, canvas, kanban
|
|
5
|
+
Project-URL: Homepage, https://github.com/ykoellmann/obsidian-mcp
|
|
6
|
+
Project-URL: Repository, https://github.com/ykoellmann/obsidian-mcp
|
|
7
|
+
Project-URL: Issues, https://github.com/ykoellmann/obsidian-mcp/issues
|
|
8
|
+
License: MIT
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
Requires-Python: >=3.12
|
|
11
|
+
Requires-Dist: fastmcp>=2.0
|
|
12
|
+
Requires-Dist: filelock>=3.13
|
|
13
|
+
Requires-Dist: python-frontmatter>=1.1
|
|
14
|
+
Requires-Dist: pyyaml>=6.0
|
|
15
|
+
Requires-Dist: watchdog>=4.0
|
|
16
|
+
Description-Content-Type: text/markdown
|
|
17
|
+
|
|
18
|
+
# obsidian-mcp
|
|
19
|
+
|
|
20
|
+
[](https://github.com/ykoellmann/obsidian-mcp/actions/workflows/ci.yml)
|
|
21
|
+
|
|
22
|
+
An MCP (Model Context Protocol) server for [Obsidian](https://obsidian.md) vaults. Connects Claude (or any MCP client) directly to your vault — read, write, search, navigate links, and manage notes, canvases, and kanban boards.
|
|
23
|
+
|
|
24
|
+
## Why obsidian-mcp?
|
|
25
|
+
|
|
26
|
+
The official Obsidian MCP plugin requires the Obsidian desktop app to be running and only works on the same machine. **obsidian-mcp is a standalone server** — no Obsidian app needed.
|
|
27
|
+
|
|
28
|
+
The intended setup is to host obsidian-mcp on a server or NAS where your vault is continuously synced (via [Syncthing](https://syncthing.net), [git](https://github.com/denolehov/obsidian-git), [rclone](https://rclone.org), or [Obsidian Sync](https://obsidian.md/sync)). Claude then connects to that server over the network, so:
|
|
29
|
+
|
|
30
|
+
- **Always up to date** — the server sees every change your Obsidian app writes, immediately
|
|
31
|
+
- **Access from anywhere** — connect from Claude Desktop, Claude Code, or any MCP client on any machine, without the vault being present locally
|
|
32
|
+
- **Multiple clients** — several Claude sessions can read the vault simultaneously; writes are serialized with per-file locking
|
|
33
|
+
- **No app dependency** — the server runs headless and starts automatically (systemd, Docker, etc.)
|
|
34
|
+
|
|
35
|
+
```
|
|
36
|
+
[Obsidian app] ──sync──► [vault on server] ◄──MCP── [Claude on any machine]
|
|
37
|
+
(phone/laptop) (NAS / VPS) (Claude Desktop / Code)
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
## Features
|
|
41
|
+
|
|
42
|
+
- **Read & Search** — read notes, search full-text (exact/regex/fuzzy), render embedded transclusions, inspect note outlines
|
|
43
|
+
- **Write** — create/overwrite notes, patch sections, append content, update frontmatter, manage tags, move notes with automatic wikilink rewriting
|
|
44
|
+
- **Folders** — list, create, delete, rename folders; renaming rewrites path-based wikilinks vault-wide
|
|
45
|
+
- **Query & Graph** — backlinks, broken links, orphan detection, BFS link graph, vault stats, task collection across vault
|
|
46
|
+
- **Dataview-like queries** — filter notes by tags, status, frontmatter fields, or inline fields (`key:: value`)
|
|
47
|
+
- **Periodic Notes** — read/preview daily, weekly, monthly, quarterly, yearly journal notes from templates
|
|
48
|
+
- **Canvas** — read, create, and patch Obsidian Canvas (`.canvas`) files
|
|
49
|
+
- **Kanban** — read, create, and manipulate Obsidian Kanban boards (columns and cards)
|
|
50
|
+
- **Attachments** — list, read (text or base64), and add binary files
|
|
51
|
+
- **Templates** — render Obsidian templates with built-in (`{{date}}`, `{{title}}`, …) and custom variables
|
|
52
|
+
- **MCP Resources** — expose vault notes, stats, and tags as MCP resources for direct context injection
|
|
53
|
+
|
|
54
|
+
## Installation
|
|
55
|
+
|
|
56
|
+
**Via uvx (no clone needed):**
|
|
57
|
+
```bash
|
|
58
|
+
VAULT_PATH=/your/vault uvx obsidian-remote-mcp
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
**Via Docker (no Python needed):**
|
|
62
|
+
```bash
|
|
63
|
+
docker compose up -d # see docker-compose.yml
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
**From source:**
|
|
67
|
+
```bash
|
|
68
|
+
git clone https://github.com/ykoellmann/obsidian-mcp.git
|
|
69
|
+
cd obsidian-mcp
|
|
70
|
+
uv sync
|
|
71
|
+
uv run obsidian-remote-mcp
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
## Configuration
|
|
75
|
+
|
|
76
|
+
Copy `.env.example` to `.env` and set your vault path:
|
|
77
|
+
|
|
78
|
+
```env
|
|
79
|
+
VAULT_PATH=/path/to/your/obsidian/vault
|
|
80
|
+
# Optional:
|
|
81
|
+
# READ_ONLY=true # prevent all writes
|
|
82
|
+
# WRITE_PATHS=Notes/,Inbox/ # restrict writes to specific folders
|
|
83
|
+
# TRANSPORT=stdio # stdio (default) or sse
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
## Usage with Claude Code
|
|
87
|
+
|
|
88
|
+
Add to your MCP config (e.g. `~/.claude/mcp.json`):
|
|
89
|
+
|
|
90
|
+
```json
|
|
91
|
+
{
|
|
92
|
+
"mcpServers": {
|
|
93
|
+
"obsidian": {
|
|
94
|
+
"command": "uv",
|
|
95
|
+
"args": ["--directory", "/path/to/obsidian-mcp", "run", "obsidian-remote-mcp"],
|
|
96
|
+
"env": {
|
|
97
|
+
"VAULT_PATH": "/path/to/your/obsidian/vault"
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
## Usage with Claude Desktop
|
|
105
|
+
|
|
106
|
+
Add to `claude_desktop_config.json`:
|
|
107
|
+
|
|
108
|
+
```json
|
|
109
|
+
{
|
|
110
|
+
"mcpServers": {
|
|
111
|
+
"obsidian": {
|
|
112
|
+
"command": "uv",
|
|
113
|
+
"args": ["--directory", "/path/to/obsidian-mcp", "run", "obsidian-remote-mcp"],
|
|
114
|
+
"env": {
|
|
115
|
+
"VAULT_PATH": "/path/to/your/obsidian/vault"
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
## Docker
|
|
123
|
+
|
|
124
|
+
```bash
|
|
125
|
+
# 1. Copy and edit the environment variables
|
|
126
|
+
cp .env.example .env
|
|
127
|
+
|
|
128
|
+
# 2. Set HOST_VAULT_PATH and API_KEY in .env, then:
|
|
129
|
+
docker compose up -d
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
The `docker-compose.yml` pulls the pre-built image from GHCR — no cloning or building required. To build locally instead, swap `image:` for `build: .` in the compose file.
|
|
133
|
+
|
|
134
|
+
## Remote Setup (Recommended)
|
|
135
|
+
|
|
136
|
+
Run obsidian-mcp on a server and connect to it remotely via SSE transport.
|
|
137
|
+
|
|
138
|
+
**1. Generate an API key:**
|
|
139
|
+
```bash
|
|
140
|
+
openssl rand -hex 32
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
**2. Configure the server** (`docker-compose.yml` or `.env`):
|
|
144
|
+
```env
|
|
145
|
+
VAULT_PATH=/data/vault
|
|
146
|
+
TRANSPORT=sse
|
|
147
|
+
API_KEY=your-generated-key
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
**3. Start:**
|
|
151
|
+
```bash
|
|
152
|
+
docker compose up -d # or: uv run obsidian-remote-mcp
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
**4. Connect from your MCP client** (anywhere on the network):
|
|
156
|
+
```json
|
|
157
|
+
{
|
|
158
|
+
"mcpServers": {
|
|
159
|
+
"obsidian": {
|
|
160
|
+
"type": "sse",
|
|
161
|
+
"url": "https://your-server/sse",
|
|
162
|
+
"headers": {
|
|
163
|
+
"Authorization": "Bearer your-generated-key"
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
> **Security:** Always put a TLS-terminating reverse proxy (e.g. [Caddy](https://caddyserver.com)) in front when exposing to the internet. `API_KEY` is not needed for stdio transport (local use only).
|
|
171
|
+
|
|
172
|
+
Keep the vault synced on the server with Syncthing, git+cron, rclone, or Obsidian Sync — obsidian-mcp picks up changes automatically via its file watcher.
|
|
173
|
+
|
|
174
|
+
## Vault Conventions (Customization)
|
|
175
|
+
|
|
176
|
+
Create `_AI_INSTRUCTIONS.md` in your vault root to teach the AI how your specific vault is organized:
|
|
177
|
+
|
|
178
|
+
```markdown
|
|
179
|
+
## Structure
|
|
180
|
+
- `Notes/` — evergreen notes and concepts
|
|
181
|
+
- `Projects/` — active and archived projects (tag: #project/active, #project/done)
|
|
182
|
+
- `Journal/` — daily notes (YYYY-MM-DD.md)
|
|
183
|
+
|
|
184
|
+
## Frontmatter Schema
|
|
185
|
+
- status: active | done | inbox
|
|
186
|
+
- tags: nested with / (e.g. #concept/programming)
|
|
187
|
+
|
|
188
|
+
## Conventions
|
|
189
|
+
- Link by stem only, never by full path
|
|
190
|
+
- Every note needs a created: date in frontmatter
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
The server loads this file at startup and sends it to the AI as system instructions. Without it, built-in generic Obsidian syntax guidance is used. The `_AI_INSTRUCTIONS.md` is the right place for everything vault-specific — folder layout, tag schema, naming conventions, and any workflow rules.
|
|
194
|
+
|
|
195
|
+
## Tool Reference
|
|
196
|
+
|
|
197
|
+
| Category | Tools |
|
|
198
|
+
|---|---|
|
|
199
|
+
| **Read** | `list_notes`, `read_note`, `search_notes`, `render_note`, `get_note_outline` |
|
|
200
|
+
| **Write** | `write_note`, `patch_note`, `delete_note`, `append_to_note`, `patch_frontmatter`, `manage_tags`, `move_note` |
|
|
201
|
+
| **Folders** | `list_folder`, `create_folder`, `delete_folder`, `rename_folder` |
|
|
202
|
+
| **Query** | `query_notes`, `get_backlinks`, `get_broken_links`, `get_orphans`, `get_link_graph`, `get_vault_stats`, `get_tasks`, `resolve_alias` |
|
|
203
|
+
| **Tags** | `get_notes_by_tag`, `get_tag_tree`, `list_all_tags` |
|
|
204
|
+
| **Periodic** | `get_daily_note`, `get_periodic_note` |
|
|
205
|
+
| **Canvas** | `list_canvases`, `read_canvas`, `write_canvas`, `patch_canvas` |
|
|
206
|
+
| **Kanban** | `read_kanban`, `create_kanban_board`, `add_kanban_card`, `move_kanban_card`, `delete_kanban_card` |
|
|
207
|
+
| **Attachments** | `list_attachments`, `read_attachment`, `add_attachment` |
|
|
208
|
+
| **Templates** | `list_templates`, `create_from_template` |
|
|
209
|
+
|
|
210
|
+
Full parameter documentation is embedded in the server and shown automatically to connected AI clients.
|
|
211
|
+
|
|
212
|
+
## Architecture
|
|
213
|
+
|
|
214
|
+
```
|
|
215
|
+
src/obsidian_mcp/
|
|
216
|
+
├── config.py # env-based config (VAULT_PATH, READ_ONLY, WRITE_PATHS)
|
|
217
|
+
├── server.py # FastMCP entry point, tool and resource registrations
|
|
218
|
+
├── domain/
|
|
219
|
+
│ ├── models.py # Note dataclass (frontmatter, tags, wikilinks, tasks, …)
|
|
220
|
+
│ ├── parser.py # Markdown parser (YAML frontmatter, wikilinks, block refs, …)
|
|
221
|
+
│ └── index.py # VaultIndex — alias resolution, backlinks, tag tree, BFS
|
|
222
|
+
├── storage/
|
|
223
|
+
│ ├── filesystem.py # atomic writes (temp + os.replace), path validation
|
|
224
|
+
│ ├── locking.py # per-file filelock to prevent concurrent write conflicts
|
|
225
|
+
│ └── watcher.py # watchdog-based vault change detection (polling fallback)
|
|
226
|
+
└── tools/
|
|
227
|
+
├── read.py # read_note, search_notes, render_note, get_note_outline
|
|
228
|
+
├── write.py # write_note, patch_note, move_note, manage_tags, …
|
|
229
|
+
├── query.py # graph tools, task aggregation, periodic notes, query_notes
|
|
230
|
+
├── folders.py # folder management
|
|
231
|
+
├── canvas.py # Obsidian Canvas (.canvas JSON) tools
|
|
232
|
+
├── kanban.py # Obsidian Kanban plugin tools
|
|
233
|
+
├── attachments.py # binary and text attachment handling
|
|
234
|
+
└── templates.py # template rendering with variable substitution
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
## Development
|
|
238
|
+
|
|
239
|
+
```bash
|
|
240
|
+
uv run pytest # run tests (238 tests)
|
|
241
|
+
uv run ruff check src/ tests/ # lint
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
## License
|
|
245
|
+
|
|
246
|
+
MIT
|