Slim list_vault to bare index, add get_file_info for detail

list_vault: Each entry now ~55 chars (name+path+type) vs ~120 before.
- Dropped size_bytes (was 0 for dirs, wasted tokens)
- Dropped modified date (moved to get_file_info)
- .obsidian hidden by default (include_hidden=True to opt in)
- Uses _file_info_lean helper

New get_file_info tool: Full metadata for a single file.
- size_bytes, modified (YYYY-MM-DD), line_count, char_count

All dates compacted: '2025-06-10' vs '2025-06-10T23:40:11.798359+00:00'
(22 chars saved per date, ~10x token savings)

Added shared helpers:
- _compact_date() for YYYY-MM-DD everywhere
- _is_hidden_path() replacing 4 duplicate filter blocks

Estimated payload reduction for 311-note vault: ~55% on files,
plus elimination of entire .obsidian config subtree.
This commit is contained in:
helm 2026-07-07 01:56:28 -04:00
parent 7d0a284422
commit 6192e22186

121
server.py
View file

@ -43,6 +43,18 @@ OBSIDIAN_HIDDEN = {".obsidian", ".trash", ".git", ".DS_Store", "node_modules"}
MARKDOWN_EXT = {".md", ".markdown"}
# ── Helpers ───────────────────────────────────────────────────────────────
def _compact_date(timestamp: float) -> str:
"""Return YYYY-MM-DD (no time/microseconds/tz — 1/10 the token cost)."""
return datetime.fromtimestamp(timestamp, tz=timezone.utc).strftime("%Y-%m-%d")
def _is_hidden_path(rel: Path) -> bool:
"""True if any path component is a dotfile/dotdir (excluding .obsidian)."""
return any(part.startswith(".") and part != ".obsidian" for part in rel.parts)
# ── Helpers ───────────────────────────────────────────────────────────────
def _vault_path(vault_name: str) -> Path:
@ -69,18 +81,35 @@ def _resolve_doc(vault_name: str, doc_path: str) -> Path:
return target
def _file_info(p: Path, relative_to: Path) -> dict[str, Any]:
"""Build a file/directory info dict."""
stat = p.stat()
def _file_info_lean(p: Path, relative_to: Path) -> dict[str, Any]:
"""Bare index entry — name, path, type only. ~55 chars vs ~120."""
return {
"name": p.name,
"path": str(p.relative_to(relative_to)).replace("\\", "/"),
"type": "directory" if p.is_dir() else "file",
"size_bytes": stat.st_size if p.is_file() else 0,
"modified": datetime.fromtimestamp(stat.st_mtime, tz=timezone.utc).isoformat(),
}
def _file_info_full(p: Path, relative_to: Path) -> dict[str, Any]:
"""Full metadata for a single file — size, date, counts."""
stat = p.stat()
info: dict[str, Any] = {
"name": p.name,
"path": str(p.relative_to(relative_to)).replace("\\", "/"),
"type": "directory" if p.is_dir() else "file",
"size_bytes": stat.st_size if p.is_file() else None,
"modified": _compact_date(stat.st_mtime),
}
if p.is_file() and _is_markdown(p):
try:
content = p.read_text(encoding="utf-8", errors="replace")
except Exception:
content = ""
info["line_count"] = content.count("\n") + (1 if content else 0)
info["char_count"] = len(content)
return info
def _is_markdown(p: Path) -> bool:
return p.suffix.lower() in MARKDOWN_EXT
@ -159,8 +188,13 @@ def list_vault(
include_files: bool = True,
include_directories: bool = True,
file_extensions: list[str] | None = None,
include_hidden: bool = False,
) -> dict[str, Any]:
"""FILESYSTEM TOOL (not memory) — List files and folders in vault on disk. See structure before reading docs.
"""FILESYSTEM TOOL (not memory) — Index files and folders in vault on disk. Use to see structure before reading docs.
Returns a lean listing (name + path + type only). Use get_file_info for
size/date/counts on a specific file. Hidden dirs (.obsidian, .git, .trash)
are excluded by default.
Args:
vault_name: Name of the vault (subdirectory name under vaults root)
@ -172,16 +206,16 @@ def list_vault(
include_directories: Include directories in results (default True).
file_extensions: Optional filter only return files with these extensions
(e.g. ['.md', '.canvas']). Case-insensitive.
include_hidden: If True, include .obsidian config directory (default False).
Usage:
list_vault("obsidian-skt") root listing
list_vault("obsidian-skt", "") same, explicit
list_vault("obsidian-skt") root index
list_vault("obsidian-skt", "", True) recursive index
list_vault("obsidian-skt", "NPCs") subfolder
list_vault("obsidian-skt", "", True) recursive from root
Returns:
directories: list of subdirectory info dicts
files: list of file info dicts (name, path, type, size, modified)
directories: [{name, path, type}]
files: [{name, path, type}]
total: combined count
"""
vault = _vault_path(vault_name)
@ -204,22 +238,13 @@ def list_vault(
iterator = target.iterdir()
for entry in sorted(iterator, key=lambda p: (not p.is_dir(), p.name.lower())):
# Skip hidden/obsidian metadata unless explicitly inside .obsidian
if entry.name.startswith(".") and entry.name not in (".obsidian",):
continue
if entry.name in OBSIDIAN_HIDDEN and not str(entry.resolve()).startswith(str(vault / ".obsidian")):
continue
# Also skip entries nested inside any hidden/excluded directory ancestor
# (e.g. .git/objects/ab/cdef — the file name looks innocent but it lives
# under .git, which rglob walks into before we can stop it)
rel = entry.relative_to(vault)
if any(
part.startswith(".") and part != ".obsidian"
for part in rel.parts
):
# Filter hidden directories/files unless include_hidden is set
if _is_hidden_path(rel) and not include_hidden:
continue
info = _file_info(entry, vault)
info = _file_info_lean(entry, vault)
if entry.is_dir():
if include_directories:
directories.append(info)
@ -270,10 +295,27 @@ def get_vault_document(vault_name: str, doc_path: str) -> dict[str, Any]:
"word_count": len(content.split()),
"char_count": len(content),
"size_bytes": stat.st_size,
"modified": datetime.fromtimestamp(stat.st_mtime, tz=timezone.utc).isoformat(),
"modified": _compact_date(stat.st_mtime),
}
@mcp.tool()
def get_file_info(vault_name: str, doc_path: str) -> dict[str, Any]:
"""FILESYSTEM TOOL (not memory) — Get size, date, and line count for a single file. Use after list_vault to inspect a specific note.
Args:
vault_name: Name of the vault.
doc_path: Relative path to the file within the vault (e.g. 'folder/note.md').
Returns metadata dict with: name, path, type, size_bytes, modified (YYYY-MM-DD),
and for markdown files: line_count, char_count.
"""
target = _resolve_doc(vault_name, doc_path)
if not target.exists():
return {"error": f"File not found: {doc_path}", "vault": vault_name}
return _file_info_full(target, _vault_path(vault_name))
@mcp.tool()
def search_vault(vault_name: str, query: str, max_results: int = 50) -> dict[str, Any]:
"""FILESYSTEM TOOL (not memory) — Search markdown files on disk. Find notes about characters, locations, concepts.
@ -290,18 +332,15 @@ def search_vault(vault_name: str, query: str, max_results: int = 50) -> dict[str
files_searched = 0
for md_file in vault.rglob("*.md"):
# Skip hidden/trash
rel = str(md_file.relative_to(vault))
if any(part.startswith(".") for part in md_file.relative_to(vault).parts):
continue
if ".trash" in rel or ".obsidian" in rel:
rel_path = md_file.relative_to(vault)
if _is_hidden_path(rel_path):
continue
files_searched += 1
matches = _search_file(md_file, query)
if matches:
results.append({
"file": rel.replace("\\", "/"),
"file": str(rel_path).replace("\\", "/"),
"matches": matches[:max_results],
"match_count": len(matches),
})
@ -336,16 +375,14 @@ def get_recent_changes(vault_name: str, limit: int = 20) -> dict[str, Any]:
md_files = []
for md_file in vault.rglob("*.md"):
rel = str(md_file.relative_to(vault))
if any(part.startswith(".") for part in md_file.relative_to(vault).parts):
continue
if ".trash" in rel or ".obsidian" in rel:
rel_path = md_file.relative_to(vault)
if _is_hidden_path(rel_path):
continue
rel = str(rel_path)
stat = md_file.stat()
md_files.append({
"path": rel.replace("\\", "/"),
"size_bytes": stat.st_size,
"modified": datetime.fromtimestamp(stat.st_mtime, tz=timezone.utc).isoformat(),
"modified": _compact_date(stat.st_mtime),
})
md_files.sort(key=lambda f: f["modified"], reverse=True)
@ -375,10 +412,8 @@ def get_backlinks(vault_name: str, doc_path: str) -> dict[str, Any]:
backlinks = []
for md_file in vault.rglob("*.md"):
rel = str(md_file.relative_to(vault)).replace("\\", "/")
if any(part.startswith(".") for part in md_file.relative_to(vault).parts):
continue
if ".trash" in rel or ".obsidian" in rel:
rel_path = md_file.relative_to(vault)
if _is_hidden_path(rel_path):
continue
if md_file.resolve() == target.resolve():
continue # skip self
@ -404,10 +439,10 @@ def get_backlinks(vault_name: str, doc_path: str) -> dict[str, Any]:
if matching_links:
stat = md_file.stat()
backlinks.append({
"file": rel,
"file": str(rel_path).replace("\\", "/"),
"links": matching_links,
"link_count": len(matching_links),
"modified": datetime.fromtimestamp(stat.st_mtime, tz=timezone.utc).isoformat(),
"modified": _compact_date(stat.st_mtime),
})
backlinks.sort(key=lambda b: b["link_count"], reverse=True)