MCP server for Obsidian — access your vault from any AI agent, even when your machine is off. Powered by Self-hosted LiveSync.
This MCP server provides AI agents with read/write access to an Obsidian vault, either locally via filesystem or remotely via CouchDB (powered by Self...
112async listNotesWithMtime(folder?: string): Promise<NoteListing[]> {
113 if (folder && !folder.endsWith("/") && !folder.endsWith("\\")) folder += "/";
114 const searchDir = folder ? await this.safePath(folder) : this.root;
115 const entries: string[] = [];
116 try {
117 for await (const entry of glob("**/*.md", { cwd: searchDir })) {
118 const full = folder ? `${folder}${entry}` : entry;
119 if (full.startsWith(".obsidian/") || full.includes("/.obsidian/")) continue;
120 entries.push(full);
121 }
122 } catch {
123 return [];
124 }
125 const results = await Promise.all(
126 entries.map(async (p) => {
127 try {
128 const s = await stat(resolve(this.root, p));
129 return { path: p, mtime: s.mtimeMs };
130 } catch {
131 return { path: p, mtime: 0 };
132 }
133 }),
134 );
135 return results.sort((a, b) => a.path.localeCompare(b.path));
136}// Exploitable via list_notes tool with a crafted folder parameter. Requires network exposure or compromised LLM.
126const results = await Promise.all(
127 entries.map(async (p) => {
128 try {
129 const s = await stat(resolve(this.root, p));
130 return { path: p, mtime: s.mtimeMs };
131 } catch {
132 return { path: p, mtime: 0 };
133 }
134 }),
135);// Exploitable via list_notes tool with a crafted folder parameter. Requires network exposure or compromised LLM.
47async writeNote(path: string, content: string): Promise<boolean> {
48 const fullPath = await this.safePath(path);
49 try {
50 await mkdir(dirname(fullPath), { recursive: true });
51 await writeFile(fullPath, content, "utf-8");
52 return true;
53 } catch {
54 return false;
55 }
56}// Exploitable only if an attacker can create symlinks in the vault (e.g., via a compromised LLM or if the vault is shared). Requires network exposure or compromised LLM.
38async readNote(path: string): Promise<string | null> {
39 const fullPath = await this.safePath(path);
40 try {
41 return await readFile(fullPath, "utf-8");
42 } catch {
43 return null;
44 }
45}// Exploitable only if an attacker can create symlinks in the vault. Requires network exposure or compromised LLM.
58async deleteNote(path: string): Promise<boolean> {
59 const fullPath = await this.safePath(path);
60 try {
61 await unlink(fullPath);
62 return true;
63 } catch {
64 return false;
65 }
66}// Exploitable only if an attacker can create symlinks in the vault. Requires network exposure or compromised LLM.
68async moveNote(from: string, to: string): Promise<boolean> {
69 const fromPath = await this.safePath(from);
70 const toPath = await this.safePath(to);
71 try {
72 await mkdir(dirname(toPath), { recursive: true });
73 await rename(fromPath, toPath);
74 return true;
75 } catch (e: any) {
76 if (e.code === "EXDEV") {
77 // Cross-device: fall back to copy-delete
78 const content = await this.readNote(from);
79 if (content === null) return false;
80 const wrote = await this.writeNote(to, content);
81 if (!wrote) return false;
82 return await this.deleteNote(from);
83 }
84 return false;
85 }
86}// Exploitable only if an attacker can create symlinks in the vault. Requires network exposure or compromised LLM.
88async getMetadata(path: string): Promise<NoteInfo | null> {
89 const fullPath = await this.safePath(path);
90 try {
91 const [content, s] = await Promise.all([
92 readFile(fullPath, "utf-8"),
93 stat(fullPath),
94 ]);
95 return {
96 path,
97 size: s.size,
98 ctime: s.birthtimeMs,
99 mtime: s.mtimeMs,
100 ...parseFrontmatterAndLinks(content),
101 };
102 } catch {
103 return null;
104 }
105}// Exploitable only if an attacker can create symlinks in the vault. Requires network exposure or compromised LLM.