@mauricio.wolff/mcp-obsidian 0.4.1 → 0.5.1
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 +29 -28
- package/dist/server.js +489 -0
- package/dist/src/filesystem.js +525 -0
- package/dist/src/frontmatter.js +104 -0
- package/dist/src/pathfilter.js +57 -0
- package/dist/src/search.js +105 -0
- package/dist/src/types.js +1 -0
- package/package.json +18 -13
- package/server.ts +3 -3
- package/src/filesystem.ts +0 -602
- package/src/frontmatter.ts +0 -124
- package/src/pathfilter.ts +0 -72
- package/src/search.ts +0 -97
- package/src/types.ts +0 -119
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { join } from 'path';
|
|
2
|
+
import { readFile, readdir } from 'node:fs/promises';
|
|
3
|
+
export class SearchService {
|
|
4
|
+
vaultPath;
|
|
5
|
+
pathFilter;
|
|
6
|
+
constructor(vaultPath, pathFilter) {
|
|
7
|
+
this.vaultPath = vaultPath;
|
|
8
|
+
this.pathFilter = pathFilter;
|
|
9
|
+
}
|
|
10
|
+
async search(params) {
|
|
11
|
+
const { query, limit = 5, searchContent = true, searchFrontmatter = false, caseSensitive = false } = params;
|
|
12
|
+
if (!query || query.trim().length === 0) {
|
|
13
|
+
throw new Error('Search query cannot be empty');
|
|
14
|
+
}
|
|
15
|
+
const results = [];
|
|
16
|
+
const maxLimit = Math.min(limit, 20);
|
|
17
|
+
// Recursively find all .md files
|
|
18
|
+
const markdownFiles = await this.findMarkdownFiles(this.vaultPath);
|
|
19
|
+
for (const fullPath of markdownFiles) {
|
|
20
|
+
// Convert absolute path back to relative path
|
|
21
|
+
const relativePath = fullPath.substring(this.vaultPath.length + 1).replace(/\\/g, '/');
|
|
22
|
+
if (!this.pathFilter.isAllowed(relativePath))
|
|
23
|
+
continue;
|
|
24
|
+
if (results.length >= maxLimit)
|
|
25
|
+
break;
|
|
26
|
+
try {
|
|
27
|
+
const content = await readFile(fullPath, 'utf-8');
|
|
28
|
+
let searchableText = '';
|
|
29
|
+
// Prepare search text based on options
|
|
30
|
+
if (searchContent && searchFrontmatter) {
|
|
31
|
+
searchableText = content;
|
|
32
|
+
}
|
|
33
|
+
else if (searchContent) {
|
|
34
|
+
// Remove frontmatter from search
|
|
35
|
+
const frontmatterMatch = content.match(/^---\n([\s\S]*?)\n---\n/);
|
|
36
|
+
searchableText = frontmatterMatch ? content.slice(frontmatterMatch[0].length) : content;
|
|
37
|
+
}
|
|
38
|
+
else if (searchFrontmatter) {
|
|
39
|
+
// Search only frontmatter
|
|
40
|
+
const frontmatterMatch = content.match(/^---\n([\s\S]*?)\n---\n/);
|
|
41
|
+
searchableText = frontmatterMatch ? frontmatterMatch[1] || '' : '';
|
|
42
|
+
}
|
|
43
|
+
const searchIn = caseSensitive ? searchableText : searchableText.toLowerCase();
|
|
44
|
+
const searchQuery = caseSensitive ? query : query.toLowerCase();
|
|
45
|
+
const index = searchIn.indexOf(searchQuery);
|
|
46
|
+
if (index !== -1) {
|
|
47
|
+
// Extract excerpt around first match
|
|
48
|
+
const excerptStart = Math.max(0, index - 50);
|
|
49
|
+
const excerptEnd = Math.min(searchableText.length, index + searchQuery.length + 50);
|
|
50
|
+
let excerpt = searchableText.slice(excerptStart, excerptEnd).trim();
|
|
51
|
+
// Add ellipsis if excerpt is truncated
|
|
52
|
+
if (excerptStart > 0)
|
|
53
|
+
excerpt = '...' + excerpt;
|
|
54
|
+
if (excerptEnd < searchableText.length)
|
|
55
|
+
excerpt = excerpt + '...';
|
|
56
|
+
// Count total matches
|
|
57
|
+
let matchCount = 0;
|
|
58
|
+
let searchIndex = 0;
|
|
59
|
+
while ((searchIndex = searchIn.indexOf(searchQuery, searchIndex)) !== -1) {
|
|
60
|
+
matchCount++;
|
|
61
|
+
searchIndex += searchQuery.length;
|
|
62
|
+
}
|
|
63
|
+
// Find line number of first match
|
|
64
|
+
const lines = searchableText.slice(0, index).split('\n');
|
|
65
|
+
const lineNumber = lines.length;
|
|
66
|
+
// Extract title from filename
|
|
67
|
+
const title = relativePath.split('/').pop()?.replace(/\.md$/, '') || relativePath;
|
|
68
|
+
results.push({
|
|
69
|
+
path: relativePath,
|
|
70
|
+
title: title,
|
|
71
|
+
excerpt: excerpt,
|
|
72
|
+
matchCount: matchCount,
|
|
73
|
+
lineNumber: lineNumber
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
catch (error) {
|
|
78
|
+
// Skip files that can't be read
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
return results;
|
|
83
|
+
}
|
|
84
|
+
async findMarkdownFiles(dirPath) {
|
|
85
|
+
const markdownFiles = [];
|
|
86
|
+
try {
|
|
87
|
+
const entries = await readdir(dirPath, { withFileTypes: true });
|
|
88
|
+
for (const entry of entries) {
|
|
89
|
+
const fullPath = join(dirPath, entry.name);
|
|
90
|
+
if (entry.isDirectory()) {
|
|
91
|
+
// Recursively search subdirectories
|
|
92
|
+
const subFiles = await this.findMarkdownFiles(fullPath);
|
|
93
|
+
markdownFiles.push(...subFiles);
|
|
94
|
+
}
|
|
95
|
+
else if (entry.isFile() && entry.name.endsWith('.md')) {
|
|
96
|
+
markdownFiles.push(fullPath);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
catch (error) {
|
|
101
|
+
// Skip directories that can't be read
|
|
102
|
+
}
|
|
103
|
+
return markdownFiles;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/package.json
CHANGED
|
@@ -1,27 +1,27 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mauricio.wolff/mcp-obsidian",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.1",
|
|
4
4
|
"description": "Lightweight MCP server for safe Obsidian vault access",
|
|
5
5
|
"author": "bitbonsai",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"type": "module",
|
|
8
8
|
"main": "server.ts",
|
|
9
9
|
"bin": {
|
|
10
|
-
"mcp-obsidian": "./server.
|
|
11
|
-
"@mauricio.wolff/mcp-obsidian": "./server.
|
|
10
|
+
"mcp-obsidian": "./dist/server.js",
|
|
11
|
+
"@mauricio.wolff/mcp-obsidian": "./dist/server.js"
|
|
12
12
|
},
|
|
13
13
|
"files": [
|
|
14
|
-
"
|
|
15
|
-
"src/**/*",
|
|
14
|
+
"dist/**/*",
|
|
16
15
|
"README.md",
|
|
17
16
|
"LICENSE"
|
|
18
17
|
],
|
|
19
18
|
"scripts": {
|
|
20
|
-
"start": "
|
|
21
|
-
"
|
|
22
|
-
"test
|
|
23
|
-
"
|
|
24
|
-
"
|
|
19
|
+
"start": "tsx server.ts",
|
|
20
|
+
"build": "tsc --project tsconfig.build.json",
|
|
21
|
+
"test": "vitest run",
|
|
22
|
+
"test:watch": "vitest",
|
|
23
|
+
"prepublishOnly": "npm run build && npm test",
|
|
24
|
+
"prepack": "npm install",
|
|
25
25
|
"publish:dry": "npm publish --dry-run",
|
|
26
26
|
"publish:beta": "npm publish --tag beta",
|
|
27
27
|
"publish:latest": "npm publish"
|
|
@@ -31,10 +31,15 @@
|
|
|
31
31
|
"gray-matter": "^4.0.3"
|
|
32
32
|
},
|
|
33
33
|
"devDependencies": {
|
|
34
|
-
"@types/
|
|
34
|
+
"@types/node": "^20.0.0",
|
|
35
|
+
"tsx": "^4.0.0",
|
|
36
|
+
"typescript": "^5.0.0",
|
|
37
|
+
"vitest": "^1.0.0",
|
|
38
|
+
"js-yaml": "^4.1.0",
|
|
39
|
+
"@types/js-yaml": "^4.0.0"
|
|
35
40
|
},
|
|
36
41
|
"engines": {
|
|
37
|
-
"
|
|
42
|
+
"node": ">=18.0.0"
|
|
38
43
|
},
|
|
39
44
|
"repository": {
|
|
40
45
|
"type": "git",
|
|
@@ -46,7 +51,7 @@
|
|
|
46
51
|
"model-context-protocol",
|
|
47
52
|
"claude",
|
|
48
53
|
"ai",
|
|
49
|
-
"
|
|
54
|
+
"node",
|
|
50
55
|
"filesystem",
|
|
51
56
|
"frontmatter",
|
|
52
57
|
"yaml"
|
package/server.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
#!/usr/bin/env
|
|
1
|
+
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
4
4
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
@@ -13,7 +13,7 @@ import { SearchService } from "./src/search.js";
|
|
|
13
13
|
|
|
14
14
|
const vaultPath = process.argv[2];
|
|
15
15
|
if (!vaultPath) {
|
|
16
|
-
console.error("Usage:
|
|
16
|
+
console.error("Usage: npx @mauricio.wolff/mcp-obsidian /path/to/vault");
|
|
17
17
|
process.exit(1);
|
|
18
18
|
}
|
|
19
19
|
|
|
@@ -25,7 +25,7 @@ const searchService = new SearchService(vaultPath, pathFilter);
|
|
|
25
25
|
|
|
26
26
|
const server = new Server({
|
|
27
27
|
name: "mcp-obsidian",
|
|
28
|
-
version: "0.
|
|
28
|
+
version: "0.5.1"
|
|
29
29
|
}, {
|
|
30
30
|
capabilities: {
|
|
31
31
|
tools: {},
|