obsidian-mcp
// src/types.ts
export interface Tool {
name: string;
description: string;
inputSchema: {
type: string;
properties: Record<string, any>;
required?: string[];
};
handler: (args: any) => Promise<{
content: Array<{
type: string;
text: string;
}>;
}>;
}
export interface ToolProvider {
getTools(): Tool[];
}
// src/tools/note-tools.ts
import { z } from "zod";
import { Tool, ToolProvider } from "../types.js";
import { promises as fs } from "fs";
import path from "path";
const CreateNoteSchema = z.object({
filename: z.string(),
content: z.string(),
folder: z.string().optional()
});
export class NoteTools implements ToolProvider {
constructor(private vaultPath: string) {}
getTools(): Tool[] {
return [
{
name: "create-note",
description: "Create a new note in the vault",
inputSchema: {
type: "object",
properties: {
filename: {
type: "string",
description: "Name of the note (with .md extension)"
},
content: {
type: "string",
description: "Content of the note in markdown format"
},
folder: {
type: "string",
description: "Optional subfolder path"
}
},
required: ["filename", "content"]
},
handler: async (args) => {
const { filename, content, folder } = CreateNoteSchema.parse(args);
const notePath = await this.createNote(filename, content, folder);
return {
content: [
{
type: "text",
text: `Successfully created note: ${notePath}`
}
]
};
}
}
];
}
private async createNote(filename: string, content: string, folder?: string): Promise<string> {
if (!filename.endsWith(".md")) {
filename = `${filename}.md`;
}
const notePath = folder
? path.join(this.vaultPath, folder, filename)
: path.join(this.vaultPath, filename);
const noteDir = path.dirname(notePath);
await fs.mkdir(noteDir, { recursive: true });
try {
await fs.access(notePath);
throw new Error("Note already exists");
} catch (error) {
if (error.code === "ENOENT") {
await fs.writeFile(notePath, content);
return notePath;
}
throw error;
}
}
}
// src/tools/search-tools.ts
import { z } from "zod";
import { Tool, ToolProvider } from "../types.js";
import { promises as fs } from "fs";
import path from "path";
const SearchSchema = z.object({
query: z.string(),
path: z.string().optional(),
caseSensitive: z.boolean().optional()
});
export class SearchTools implements ToolProvider {
constructor(private vaultPath: string) {}
getTools(): Tool[] {
return [
{
name: "search-vault",
description: "Search for text across notes",
inputSchema: {
type: "object",
properties: {
query: {
type: "string",
description: "Search query"
},
path: {
type: "string",
description: "Optional path to limit search scope"
},
caseSensitive: {
type: "boolean",
description: "Whether to perform case-sensitive search"
}
},
required: ["query"]
},
handler: async (args) => {
const { query, path: searchPath, caseSensitive } = SearchSchema.parse(args);
const results = await this.searchVault(query, searchPath, caseSensitive);
return {
content: [
{
type: "text",
text: this.formatSearchResults(results)
}
]
};
}
}
];
}
private async searchVault(query: string, searchPath?: string, caseSensitive = false) {
// Implementation of searchVault method...
}
private formatSearchResults(results: any[]) {
// Implementation of formatSearchResults method...
}
}
// src/server.ts
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 { Tool, ToolProvider } from "./types.js";
export class ObsidianServer {
private server: Server;
private tools: Map<string, Tool> = new Map();
constructor() {
this.server = new Server(
{
name: "obsidian-vault",
version: "1.0.0"
},
{
capabilities: {
tools: {}
}
}
);
this.setupHandlers();
}
registerToolProvider(provider: ToolProvider) {
for (const tool of provider.getTools()) {
this.tools.set(tool.name, tool);
}
}
private setupHandlers() {
this.server.setRequestHandler(ListToolsRequestSchema, async () => ({
tools: Array.from(this.tools.values()).map(tool => ({
name: tool.name,
description: tool.description,
inputSchema: tool.inputSchema
}))
}));
this.server.setRequestHandler(CallToolRequestSchema, async (request) => {
const { name, arguments: args } = request.params;
const tool = this.tools.get(name);
if (!tool) {
throw new Error(`Unknown tool: ${name}`);
}
return tool.handler(args);
});
}
async start() {
const transport = new StdioServerTransport();
await this.server.connect(transport);
console.error("Obsidian MCP Server running on stdio");
}
}
// src/main.ts
import { ObsidianServer } from "./server.js";
import { NoteTools } from "./tools/note-tools.js";
import { SearchTools } from "./tools/search-tools.js";
async function main() {
const vaultPath = process.argv[2];
if (!vaultPath) {
console.error("Please provide the path to your Obsidian vault");
process.exit(1);
}
try {
const server = new ObsidianServer();
// Register tool providers
server.registerToolProvider(new NoteTools(vaultPath));
server.registerToolProvider(new SearchTools(vaultPath));
await server.start();
} catch (error) {
console.error("Fatal error:", error);
process.exit(1);
}
}
main().catch((error) => {
console.error("Unhandled error:", error);
process.exit(1);
});