added all mcp
This commit is contained in:
393
obsidian-mcp-server/dist/index.js
vendored
Normal file
393
obsidian-mcp-server/dist/index.js
vendored
Normal file
@@ -0,0 +1,393 @@
|
||||
#!/usr/bin/env node
|
||||
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
||||
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
||||
import { CallToolRequestSchema, ListToolsRequestSchema, } from "@modelcontextprotocol/sdk/types.js";
|
||||
import fs from "fs/promises";
|
||||
import path from "path";
|
||||
import { glob } from "glob";
|
||||
// Configuration
|
||||
const VAULT_PATH = process.env.OBSIDIAN_VAULT_PATH || "/path/to/your/vault";
|
||||
class ObsidianMCPServer {
|
||||
server;
|
||||
constructor() {
|
||||
this.server = new Server({
|
||||
name: "obsidian-mcp-server",
|
||||
version: "1.0.0",
|
||||
}, {
|
||||
capabilities: {
|
||||
tools: {},
|
||||
},
|
||||
});
|
||||
this.setupHandlers();
|
||||
}
|
||||
setupHandlers() {
|
||||
// List available tools
|
||||
this.server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
||||
tools: [
|
||||
{
|
||||
name: "search_notes",
|
||||
description: "Search for notes in the Obsidian vault by content or filename",
|
||||
inputSchema: {
|
||||
type: "object",
|
||||
properties: {
|
||||
query: {
|
||||
type: "string",
|
||||
description: "Search query to find in note content or filename",
|
||||
},
|
||||
search_type: {
|
||||
type: "string",
|
||||
enum: ["content", "filename", "both"],
|
||||
description: "Type of search to perform",
|
||||
default: "both",
|
||||
},
|
||||
},
|
||||
required: ["query"],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "read_note",
|
||||
description: "Read the content of a specific note",
|
||||
inputSchema: {
|
||||
type: "object",
|
||||
properties: {
|
||||
note_path: {
|
||||
type: "string",
|
||||
description: "Relative path to the note within the vault (e.g., 'folder/note.md')",
|
||||
},
|
||||
},
|
||||
required: ["note_path"],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "create_note",
|
||||
description: "Create a new note in the Obsidian vault",
|
||||
inputSchema: {
|
||||
type: "object",
|
||||
properties: {
|
||||
note_path: {
|
||||
type: "string",
|
||||
description: "Relative path for the new note (e.g., 'folder/note.md')",
|
||||
},
|
||||
content: {
|
||||
type: "string",
|
||||
description: "Content of the note in Markdown format",
|
||||
},
|
||||
},
|
||||
required: ["note_path", "content"],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "update_note",
|
||||
description: "Update an existing note's content",
|
||||
inputSchema: {
|
||||
type: "object",
|
||||
properties: {
|
||||
note_path: {
|
||||
type: "string",
|
||||
description: "Relative path to the note (e.g., 'folder/note.md')",
|
||||
},
|
||||
content: {
|
||||
type: "string",
|
||||
description: "New content for the note",
|
||||
},
|
||||
},
|
||||
required: ["note_path", "content"],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "delete_note",
|
||||
description: "Delete a note from the vault",
|
||||
inputSchema: {
|
||||
type: "object",
|
||||
properties: {
|
||||
note_path: {
|
||||
type: "string",
|
||||
description: "Relative path to the note to delete",
|
||||
},
|
||||
},
|
||||
required: ["note_path"],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "list_notes",
|
||||
description: "List all notes in the vault or in a specific folder",
|
||||
inputSchema: {
|
||||
type: "object",
|
||||
properties: {
|
||||
folder: {
|
||||
type: "string",
|
||||
description: "Optional folder path to list notes from (relative to vault root)",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "get_tags",
|
||||
description: "Extract all tags from notes in the vault",
|
||||
inputSchema: {
|
||||
type: "object",
|
||||
properties: {
|
||||
note_path: {
|
||||
type: "string",
|
||||
description: "Optional: specific note to get tags from. If not provided, gets all tags from vault",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "append_to_note",
|
||||
description: "Append content to an existing note",
|
||||
inputSchema: {
|
||||
type: "object",
|
||||
properties: {
|
||||
note_path: {
|
||||
type: "string",
|
||||
description: "Relative path to the note",
|
||||
},
|
||||
content: {
|
||||
type: "string",
|
||||
description: "Content to append",
|
||||
},
|
||||
},
|
||||
required: ["note_path", "content"],
|
||||
},
|
||||
},
|
||||
],
|
||||
}));
|
||||
// Handle tool calls
|
||||
this.server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
||||
const { name, arguments: args } = request.params;
|
||||
try {
|
||||
if (!args) {
|
||||
throw new Error("Missing arguments");
|
||||
}
|
||||
switch (name) {
|
||||
case "search_notes":
|
||||
return await this.searchNotes(args.query, args.search_type || "both");
|
||||
case "read_note":
|
||||
return await this.readNote(args.note_path);
|
||||
case "create_note":
|
||||
return await this.createNote(args.note_path, args.content);
|
||||
case "update_note":
|
||||
return await this.updateNote(args.note_path, args.content);
|
||||
case "delete_note":
|
||||
return await this.deleteNote(args.note_path);
|
||||
case "list_notes":
|
||||
return await this.listNotes(args.folder);
|
||||
case "get_tags":
|
||||
return await this.getTags(args.note_path);
|
||||
case "append_to_note":
|
||||
return await this.appendToNote(args.note_path, args.content);
|
||||
default:
|
||||
throw new Error(`Unknown tool: ${name}`);
|
||||
}
|
||||
}
|
||||
catch (error) {
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: `Error: ${error instanceof Error ? error.message : String(error)}`,
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
});
|
||||
}
|
||||
async searchNotes(query, searchType) {
|
||||
const pattern = path.join(VAULT_PATH, "**/*.md");
|
||||
const files = await glob(pattern, { nodir: true });
|
||||
const results = [];
|
||||
for (const file of files) {
|
||||
const relativePath = path.relative(VAULT_PATH, file);
|
||||
const content = await fs.readFile(file, "utf-8");
|
||||
const matches = [];
|
||||
if (searchType === "filename" || searchType === "both") {
|
||||
if (relativePath.toLowerCase().includes(query.toLowerCase())) {
|
||||
matches.push(`Filename match: ${relativePath}`);
|
||||
}
|
||||
}
|
||||
if (searchType === "content" || searchType === "both") {
|
||||
const lines = content.split("\n");
|
||||
lines.forEach((line, index) => {
|
||||
if (line.toLowerCase().includes(query.toLowerCase())) {
|
||||
matches.push(`Line ${index + 1}: ${line.trim()}`);
|
||||
}
|
||||
});
|
||||
}
|
||||
if (matches.length > 0) {
|
||||
results.push({ path: relativePath, matches });
|
||||
}
|
||||
}
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: JSON.stringify({ query, results, count: results.length }, null, 2),
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
async readNote(notePath) {
|
||||
const fullPath = path.join(VAULT_PATH, notePath);
|
||||
// Security check: ensure path is within vault
|
||||
if (!fullPath.startsWith(VAULT_PATH)) {
|
||||
throw new Error("Invalid path: outside vault");
|
||||
}
|
||||
const content = await fs.readFile(fullPath, "utf-8");
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: content,
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
async createNote(notePath, content) {
|
||||
const fullPath = path.join(VAULT_PATH, notePath);
|
||||
if (!fullPath.startsWith(VAULT_PATH)) {
|
||||
throw new Error("Invalid path: outside vault");
|
||||
}
|
||||
// Create directory if it doesn't exist
|
||||
await fs.mkdir(path.dirname(fullPath), { recursive: true });
|
||||
// Check if file already exists
|
||||
try {
|
||||
await fs.access(fullPath);
|
||||
throw new Error(`Note already exists: ${notePath}`);
|
||||
}
|
||||
catch (error) {
|
||||
if (error.code !== "ENOENT") {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
await fs.writeFile(fullPath, content, "utf-8");
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: `Note created successfully: ${notePath}`,
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
async updateNote(notePath, content) {
|
||||
const fullPath = path.join(VAULT_PATH, notePath);
|
||||
if (!fullPath.startsWith(VAULT_PATH)) {
|
||||
throw new Error("Invalid path: outside vault");
|
||||
}
|
||||
await fs.writeFile(fullPath, content, "utf-8");
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: `Note updated successfully: ${notePath}`,
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
async deleteNote(notePath) {
|
||||
const fullPath = path.join(VAULT_PATH, notePath);
|
||||
if (!fullPath.startsWith(VAULT_PATH)) {
|
||||
throw new Error("Invalid path: outside vault");
|
||||
}
|
||||
await fs.unlink(fullPath);
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: `Note deleted successfully: ${notePath}`,
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
async listNotes(folder) {
|
||||
const searchPath = folder ? path.join(VAULT_PATH, folder) : VAULT_PATH;
|
||||
const pattern = path.join(searchPath, "**/*.md");
|
||||
const files = await glob(pattern, { nodir: true });
|
||||
const notes = files.map((file) => {
|
||||
const relativePath = path.relative(VAULT_PATH, file);
|
||||
return {
|
||||
path: relativePath,
|
||||
name: path.basename(file, ".md"),
|
||||
};
|
||||
});
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: JSON.stringify({ notes, count: notes.length }, null, 2),
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
async getTags(notePath) {
|
||||
const allTags = new Set();
|
||||
if (notePath) {
|
||||
const fullPath = path.join(VAULT_PATH, notePath);
|
||||
const content = await fs.readFile(fullPath, "utf-8");
|
||||
const tags = this.extractTags(content);
|
||||
tags.forEach((tag) => allTags.add(tag));
|
||||
}
|
||||
else {
|
||||
const pattern = path.join(VAULT_PATH, "**/*.md");
|
||||
const files = await glob(pattern, { nodir: true });
|
||||
for (const file of files) {
|
||||
const content = await fs.readFile(file, "utf-8");
|
||||
const tags = this.extractTags(content);
|
||||
tags.forEach((tag) => allTags.add(tag));
|
||||
}
|
||||
}
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: JSON.stringify({ tags: Array.from(allTags).sort() }, null, 2),
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
extractTags(content) {
|
||||
const tags = [];
|
||||
// Extract inline tags (#tag)
|
||||
const inlineTagRegex = /#([a-zA-Z0-9_/-]+)/g;
|
||||
let match;
|
||||
while ((match = inlineTagRegex.exec(content)) !== null) {
|
||||
tags.push(match[1]);
|
||||
}
|
||||
// Extract frontmatter tags
|
||||
const frontmatterRegex = /^---\n([\s\S]*?)\n---/;
|
||||
const frontmatterMatch = content.match(frontmatterRegex);
|
||||
if (frontmatterMatch) {
|
||||
const frontmatter = frontmatterMatch[1];
|
||||
const tagsMatch = frontmatter.match(/tags:\s*\[(.*?)\]/);
|
||||
if (tagsMatch) {
|
||||
const fmTags = tagsMatch[1].split(",").map((t) => t.trim().replace(/['"]/g, ""));
|
||||
tags.push(...fmTags);
|
||||
}
|
||||
}
|
||||
return [...new Set(tags)];
|
||||
}
|
||||
async appendToNote(notePath, content) {
|
||||
const fullPath = path.join(VAULT_PATH, notePath);
|
||||
if (!fullPath.startsWith(VAULT_PATH)) {
|
||||
throw new Error("Invalid path: outside vault");
|
||||
}
|
||||
await fs.appendFile(fullPath, "\n" + content, "utf-8");
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: `Content appended successfully to: ${notePath}`,
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
async run() {
|
||||
const transport = new StdioServerTransport();
|
||||
await this.server.connect(transport);
|
||||
console.error("Obsidian MCP server running on stdio");
|
||||
}
|
||||
}
|
||||
const server = new ObsidianMCPServer();
|
||||
server.run().catch(console.error);
|
||||
Reference in New Issue
Block a user