obsidian-mcp-local 1.0.0

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.
package/README.md ADDED
@@ -0,0 +1,160 @@
1
+ # Obsidian MCP Local
2
+
3
+ MCP local em **Node.js + TypeScript** para expor seu **vault do Obsidian** ao **VS Code + GitHub Copilot**.
4
+
5
+ Ele foi pensado para uso local via **stdio**, com foco em ler e escrever notas Markdown dentro do seu vault.
6
+
7
+ ## Install
8
+
9
+ npm install -g obsidian-mcp-local
10
+
11
+ ## Features
12
+
13
+ ### Tools disponíveis
14
+
15
+ - `search_notes(query)`
16
+ - busca por texto no path, frontmatter e conteúdo das notas
17
+ - retorna resultados ranqueados com pequeno excerpt
18
+
19
+ - `get_note(path)`
20
+ - abre uma nota do vault
21
+ - retorna `path`, `frontmatter` e `content`
22
+
23
+ - `create_note(path, content, overwrite?)`
24
+ - cria uma nota nova
25
+ - opcionalmente sobrescreve uma nota existente
26
+
27
+ - `append_to_note(path, content)`
28
+ - adiciona conteúdo no final de uma nota existente
29
+
30
+ - `find_by_tag(tag)`
31
+ - encontra notas por tag
32
+ - suporta `tags` no frontmatter e tags inline no conteúdo
33
+
34
+ ## Regras implementadas
35
+
36
+ - só acessa arquivos **dentro do vault configurado**
37
+ - ignora diretórios como:
38
+ - `.obsidian`
39
+ - `.git`
40
+ - `node_modules`
41
+ - trabalha apenas com arquivos `.md`
42
+ - normaliza paths para evitar acesso fora do diretório base
43
+
44
+ ## Estrutura do projeto
45
+
46
+ ```txt
47
+ obsidian-mcp-local/
48
+ package.json
49
+ tsconfig.json
50
+ README.md
51
+ .vscode/
52
+ mcp.example.json
53
+ src/
54
+ index.ts
55
+ ```
56
+
57
+ ## Pré-requisitos
58
+
59
+ - Node.js 20+
60
+ - npm
61
+ - VS Code com GitHub Copilot
62
+ - um vault do Obsidian local
63
+
64
+ ## Instalação
65
+
66
+ No diretório do projeto:
67
+
68
+ ```bash
69
+ npm install
70
+ npm run build
71
+ ```
72
+
73
+ Para desenvolvimento:
74
+
75
+ ```bash
76
+ npm run dev
77
+ ```
78
+
79
+ Para rodar a versão compilada:
80
+
81
+ ```bash
82
+ npm start
83
+ ```
84
+
85
+ ## Como usar no VS Code
86
+
87
+ ### 1. Compile o projeto
88
+
89
+ ```bash
90
+ npm install
91
+ npm run build
92
+ ```
93
+
94
+ ### 2. Ajuste o arquivo MCP do VS Code
95
+
96
+ Copie o conteúdo de `.vscode/mcp.example.json` para o seu `.vscode/mcp.json` no workspace onde você vai usar o Copilot.
97
+
98
+ Exemplo:
99
+
100
+ ```json
101
+ {
102
+ "servers": {
103
+ "obsidian-local-vault": {
104
+ "type": "stdio",
105
+ "command": "node",
106
+ "args": ["C:/caminho/para/obsidian-mcp-local/dist/index.js"],
107
+ "env": {
108
+ "OBSIDIAN_VAULT_PATH": "D:/Obsidian/Vault"
109
+ }
110
+ }
111
+ }
112
+ }
113
+ ```
114
+
115
+ ### 3. Atualize os caminhos
116
+
117
+ Substitua:
118
+
119
+ - `C:/caminho/para/obsidian-mcp-local/dist/index.js`
120
+ - `D:/Obsidian/Vault`
121
+
122
+ pelos caminhos reais da sua máquina.
123
+
124
+ ### 4. Reinicie/recarrregue o VS Code
125
+
126
+ Depois disso, o Copilot deve descobrir o servidor MCP.
127
+
128
+ ## Exemplos de uso no Copilot Chat
129
+
130
+ - “Procure no meu vault notas sobre .NET”
131
+ - “Abra a nota `knowledge/backend/dotnet.md`”
132
+ - “Crie uma nota em `inbox/ideias-mcp.md` com um resumo do que discutimos”
133
+ - “Adicione no final da nota `daily/2026-04-06.md` o texto `- testar MCP local`”
134
+ - “Encontre notas com a tag `#arquitetura`”
135
+
136
+ ## Possíveis melhorias futuras
137
+
138
+ - `append_under_heading`
139
+ - parsing de `[[wikilinks]]`
140
+ - `get_backlinks(note)`
141
+ - índice em SQLite para busca rápida
142
+ - whitelist de pastas para escrita (`inbox/`, `daily/`, `scratch/`)
143
+ - bloqueio configurável de escrita em determinadas pastas
144
+
145
+ ## Observações importantes
146
+
147
+ - Este projeto **não depende do Obsidian aberto**.
148
+ - Ele opera diretamente sobre os arquivos do vault.
149
+ - Se você habilitar escrita tanto no Obsidian quanto no VS Code, o controle de concorrência fica por sua conta.
150
+ - O projeto hoje assume que o vault é uma pasta Markdown local.
151
+
152
+ ## Arquivo principal
153
+
154
+ A implementação está em:
155
+
156
+ - `src/index.ts`
157
+
158
+ ## Licença
159
+
160
+ Uso pessoal / base inicial para customização.
package/dist/index.js ADDED
@@ -0,0 +1,13 @@
1
+ #!/usr/bin/env node
2
+ import "dotenv/config";
3
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
4
+ import { createServer } from "./server.js";
5
+ async function main() {
6
+ const server = createServer();
7
+ const transport = new StdioServerTransport();
8
+ await server.connect(transport);
9
+ }
10
+ main().catch((error) => {
11
+ console.error("[obsidian-mcp-local] Fatal error:", error);
12
+ process.exit(1);
13
+ });
package/dist/server.js ADDED
@@ -0,0 +1,18 @@
1
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import * as searchNotes from "./tools/search-notes.js";
3
+ import * as getNote from "./tools/get-note.js";
4
+ import * as createNote from "./tools/create-note.js";
5
+ import * as appendToNote from "./tools/append-to-note.js";
6
+ import * as findByTag from "./tools/find-by-tag.js";
7
+ export function createServer() {
8
+ const server = new McpServer({
9
+ name: "obsidian-local-vault",
10
+ version: "1.0.0",
11
+ });
12
+ searchNotes.register(server);
13
+ getNote.register(server);
14
+ createNote.register(server);
15
+ appendToNote.register(server);
16
+ findByTag.register(server);
17
+ return server;
18
+ }
@@ -0,0 +1,15 @@
1
+ import { z } from "zod";
2
+ import { appendToNote } from "../vault/notes.js";
3
+ export function register(server) {
4
+ server.registerTool("append_to_note", {
5
+ inputSchema: {
6
+ path: z.string().min(1),
7
+ content: z.string().min(1),
8
+ },
9
+ }, async ({ path, content }) => {
10
+ const result = await appendToNote(path, content);
11
+ return {
12
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
13
+ };
14
+ });
15
+ }
@@ -0,0 +1,16 @@
1
+ import { z } from "zod";
2
+ import { createNote } from "../vault/notes.js";
3
+ export function register(server) {
4
+ server.registerTool("create_note", {
5
+ inputSchema: {
6
+ path: z.string().min(1),
7
+ content: z.string(),
8
+ overwrite: z.boolean().optional(),
9
+ },
10
+ }, async ({ path, content, overwrite }) => {
11
+ const result = await createNote(path, content, overwrite ?? false);
12
+ return {
13
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
14
+ };
15
+ });
16
+ }
@@ -0,0 +1,10 @@
1
+ import { z } from "zod";
2
+ import { findByTag } from "../vault/notes.js";
3
+ export function register(server) {
4
+ server.registerTool("find_by_tag", { inputSchema: { tag: z.string().min(1) } }, async ({ tag }) => {
5
+ const results = await findByTag(tag);
6
+ return {
7
+ content: [{ type: "text", text: JSON.stringify(results, null, 2) }],
8
+ };
9
+ });
10
+ }
@@ -0,0 +1,10 @@
1
+ import { z } from "zod";
2
+ import { readNote } from "../vault/notes.js";
3
+ export function register(server) {
4
+ server.registerTool("get_note", { inputSchema: { path: z.string().min(1) } }, async ({ path }) => {
5
+ const note = await readNote(path);
6
+ return {
7
+ content: [{ type: "text", text: JSON.stringify(note, null, 2) }],
8
+ };
9
+ });
10
+ }
@@ -0,0 +1,10 @@
1
+ import { z } from "zod";
2
+ import { searchNotes } from "../vault/notes.js";
3
+ export function register(server) {
4
+ server.registerTool("search_notes", { inputSchema: { query: z.string().min(1) } }, async ({ query }) => {
5
+ const results = await searchNotes(query);
6
+ return {
7
+ content: [{ type: "text", text: JSON.stringify(results, null, 2) }],
8
+ };
9
+ });
10
+ }
@@ -0,0 +1,32 @@
1
+ import fs from "node:fs/promises";
2
+ import path from "node:path";
3
+ import { isMarkdownFile } from "./path-utils.js";
4
+ export async function pathExists(filePath) {
5
+ try {
6
+ await fs.access(filePath);
7
+ return true;
8
+ }
9
+ catch {
10
+ return false;
11
+ }
12
+ }
13
+ export async function walkMarkdownFiles(dir) {
14
+ const entries = await fs.readdir(dir, { withFileTypes: true });
15
+ const files = [];
16
+ for (const entry of entries) {
17
+ const fullPath = path.join(dir, entry.name);
18
+ if (entry.isDirectory()) {
19
+ if (entry.name === ".obsidian" ||
20
+ entry.name === ".git" ||
21
+ entry.name === "node_modules") {
22
+ continue;
23
+ }
24
+ files.push(...(await walkMarkdownFiles(fullPath)));
25
+ continue;
26
+ }
27
+ if (entry.isFile() && isMarkdownFile(entry.name)) {
28
+ files.push(fullPath);
29
+ }
30
+ }
31
+ return files;
32
+ }
@@ -0,0 +1,112 @@
1
+ import fs from "node:fs/promises";
2
+ import path from "node:path";
3
+ import matter from "gray-matter";
4
+ import { VAULT_ROOT, ensureMdExtension, resolveVaultPath, toRelativeVaultPath, } from "./path-utils.js";
5
+ import { pathExists, walkMarkdownFiles } from "./fs-utils.js";
6
+ export async function readNote(relativePath) {
7
+ const finalPath = ensureMdExtension(relativePath);
8
+ const fullPath = resolveVaultPath(finalPath);
9
+ if (!(await pathExists(fullPath))) {
10
+ throw new Error(`Note not found: ${finalPath}`);
11
+ }
12
+ const raw = await fs.readFile(fullPath, "utf-8");
13
+ const parsed = matter(raw);
14
+ return {
15
+ path: finalPath,
16
+ frontmatter: parsed.data,
17
+ content: parsed.content,
18
+ };
19
+ }
20
+ export async function searchNotes(query) {
21
+ const q = query.trim().toLowerCase();
22
+ if (!q)
23
+ return [];
24
+ const files = await walkMarkdownFiles(VAULT_ROOT);
25
+ const matches = [];
26
+ for (const fullPath of files) {
27
+ const raw = await fs.readFile(fullPath, "utf-8");
28
+ const parsed = matter(raw);
29
+ const relativePath = toRelativeVaultPath(fullPath);
30
+ const haystack = `${relativePath}\n${JSON.stringify(parsed.data)}\n${parsed.content}`.toLowerCase();
31
+ const idx = haystack.indexOf(q);
32
+ if (idx >= 0) {
33
+ const contentLower = parsed.content.toLowerCase();
34
+ const contentIdx = contentLower.indexOf(q);
35
+ const excerpt = contentIdx >= 0
36
+ ? parsed.content
37
+ .slice(Math.max(0, contentIdx - 120), Math.min(parsed.content.length, contentIdx + 220))
38
+ .trim()
39
+ : "";
40
+ let score = 1;
41
+ if (relativePath.toLowerCase().includes(q))
42
+ score += 4;
43
+ if (JSON.stringify(parsed.data).toLowerCase().includes(q))
44
+ score += 2;
45
+ if (contentIdx >= 0)
46
+ score += 1;
47
+ matches.push({ path: relativePath, score, excerpt });
48
+ }
49
+ }
50
+ return matches
51
+ .sort((a, b) => b.score - a.score || a.path.localeCompare(b.path))
52
+ .slice(0, 20);
53
+ }
54
+ export async function findByTag(tag) {
55
+ const normalizedTag = tag.replace(/^#/, "").trim().toLowerCase();
56
+ if (!normalizedTag)
57
+ return [];
58
+ const files = await walkMarkdownFiles(VAULT_ROOT);
59
+ const results = [];
60
+ for (const fullPath of files) {
61
+ const raw = await fs.readFile(fullPath, "utf-8");
62
+ const parsed = matter(raw);
63
+ const relativePath = toRelativeVaultPath(fullPath);
64
+ const matchedIn = [];
65
+ const fmTags = parsed.data?.tags;
66
+ if (Array.isArray(fmTags)) {
67
+ const found = fmTags.some((t) => String(t).replace(/^#/, "").toLowerCase() === normalizedTag);
68
+ if (found)
69
+ matchedIn.push("frontmatter.tags");
70
+ }
71
+ const inlineTagRegex = /(^|\s)#([a-zA-Z0-9/_-]+)/g;
72
+ for (const match of parsed.content.matchAll(inlineTagRegex)) {
73
+ const foundTag = match[2]?.toLowerCase();
74
+ if (foundTag === normalizedTag) {
75
+ matchedIn.push("content");
76
+ break;
77
+ }
78
+ }
79
+ if (matchedIn.length > 0) {
80
+ results.push({ path: relativePath, matchedIn });
81
+ }
82
+ }
83
+ return results.sort((a, b) => a.path.localeCompare(b.path));
84
+ }
85
+ export async function createNote(relativePath, content, overwrite = false) {
86
+ const finalPath = ensureMdExtension(relativePath);
87
+ const fullPath = resolveVaultPath(finalPath);
88
+ await fs.mkdir(path.dirname(fullPath), { recursive: true });
89
+ const exists = await pathExists(fullPath);
90
+ if (exists && !overwrite) {
91
+ throw new Error(`Note already exists: ${finalPath}`);
92
+ }
93
+ await fs.writeFile(fullPath, content, "utf-8");
94
+ return {
95
+ path: finalPath,
96
+ created: !exists,
97
+ overwritten: exists && overwrite,
98
+ };
99
+ }
100
+ export async function appendToNote(relativePath, content) {
101
+ const finalPath = ensureMdExtension(relativePath);
102
+ const fullPath = resolveVaultPath(finalPath);
103
+ if (!(await pathExists(fullPath))) {
104
+ throw new Error(`Note not found: ${finalPath}`);
105
+ }
106
+ const prefix = content.startsWith("\n") ? "" : "\n";
107
+ await fs.appendFile(fullPath, `${prefix}${content}`, "utf-8");
108
+ return {
109
+ path: finalPath,
110
+ appended: true,
111
+ };
112
+ }
@@ -0,0 +1,30 @@
1
+ import path from "node:path";
2
+ const vaultPath = process.env.OBSIDIAN_VAULT_PATH;
3
+ if (!vaultPath) {
4
+ throw new Error("Environment variable OBSIDIAN_VAULT_PATH is required.");
5
+ }
6
+ export const VAULT_ROOT = path.resolve(vaultPath);
7
+ export function normalizeSlashes(input) {
8
+ return input.replace(/\\/g, "/");
9
+ }
10
+ export function isMarkdownFile(filePath) {
11
+ return filePath.toLowerCase().endsWith(".md");
12
+ }
13
+ export function ensureMdExtension(filePath) {
14
+ return isMarkdownFile(filePath) ? filePath : `${filePath}.md`;
15
+ }
16
+ export function resolveVaultPath(relativePath) {
17
+ const safeRelativePath = normalizeSlashes(relativePath).replace(/^\/+/, "");
18
+ const fullPath = path.resolve(VAULT_ROOT, safeRelativePath);
19
+ const rootWithSep = VAULT_ROOT.endsWith(path.sep)
20
+ ? VAULT_ROOT
21
+ : `${VAULT_ROOT}${path.sep}`;
22
+ const isInsideVault = fullPath === VAULT_ROOT || fullPath.startsWith(rootWithSep);
23
+ if (!isInsideVault) {
24
+ throw new Error("Access outside the vault is not allowed.");
25
+ }
26
+ return fullPath;
27
+ }
28
+ export function toRelativeVaultPath(fullPath) {
29
+ return normalizeSlashes(path.relative(VAULT_ROOT, fullPath));
30
+ }
package/package.json ADDED
@@ -0,0 +1,41 @@
1
+ {
2
+ "name": "obsidian-mcp-local",
3
+ "version": "1.0.0",
4
+ "private": false,
5
+ "author": "Henrique Carvalho de Souza",
6
+ "license": "MIT",
7
+ "description": "MCP local para um vault do Obsidian, pensado para uso com VS Code + GitHub Copilot.",
8
+ "type": "module",
9
+ "main": "dist/index.js",
10
+ "bin": {
11
+ "obsidian-mcp-local": "./dist/index.js"
12
+ },
13
+ "files": [
14
+ "dist"
15
+ ],
16
+ "scripts": {
17
+ "build": "tsc -p tsconfig.json",
18
+ "dev": "tsx src/index.ts",
19
+ "start": "node dist/index.js",
20
+ "prepublishOnly": "npm run build"
21
+ },
22
+ "keywords": [
23
+ "mcp",
24
+ "model-context-protocol",
25
+ "obsidian",
26
+ "copilot",
27
+ "ai",
28
+ "knowledge-base"
29
+ ],
30
+ "dependencies": {
31
+ "@modelcontextprotocol/sdk": "^1.11.4",
32
+ "dotenv": "^17.4.1",
33
+ "gray-matter": "^4.0.3",
34
+ "zod": "^3.24.3"
35
+ },
36
+ "devDependencies": {
37
+ "@types/node": "^22.19.17",
38
+ "tsx": "^4.19.3",
39
+ "typescript": "^5.8.2"
40
+ }
41
+ }