import { Server } from '@modelcontextprotocol/sdk/server/index.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import {
CallToolRequestSchema,
ListToolsRequestSchema,
Tool,
} from '@modelcontextprotocol/sdk/types.js';
import { ConfigManager } from './config/manager.js';
import { FileLockManager } from './core/lock-manager.js';
import { VaultResolver } from './core/vault-resolver.js';
import { VaultManager } from './core/vault-manager.js';
import { OperationRouter } from './core/operation-router.js';
import { CredentialManager } from './security/credential-manager.js';
import { RateLimiter } from './security/rate-limiter.js';
import { FilesystemProvider } from './providers/filesystem-provider.js';
import { RestApiProvider } from './providers/rest-api-provider.js';
import { MCPTools } from './tools/index.js';
export class ObsidianMCPServer {
private server: Server;
private mcpTools: MCPTools;
private lockManager: FileLockManager;
private configManager: ConfigManager;
constructor(configPath?: string) {
this.configManager = new ConfigManager(configPath);
const config = this.configManager.getConfig();
this.configManager.validate();
// Initialize core components
this.lockManager = new FileLockManager(config.features.fileLocking.cleanupInterval);
const credentialManager = new CredentialManager();
const vaultResolver = new VaultResolver(config, this.configManager);
const vaultManager = new VaultManager(config, credentialManager);
const rateLimiter = new RateLimiter(config.security.rateLimit);
// Initialize providers
const filesystemProvider = new FilesystemProvider(this.lockManager);
const restApiProvider = new RestApiProvider();
// Initialize router
const operationRouter = new OperationRouter(
config,
filesystemProvider,
restApiProvider,
credentialManager,
rateLimiter
);
// Initialize tools
this.mcpTools = new MCPTools(vaultResolver, vaultManager, operationRouter);
// Initialize MCP server
this.server = new Server(
{
name: 'obsidian-mcp-server',
version: '1.0.0',
},
{
capabilities: {
tools: {},
},
}
);
this.setupHandlers();
}
private setupHandlers(): void {
// List available tools
this.server.setRequestHandler(ListToolsRequestSchema, async () => {
return {
tools: this.getToolDefinitions(),
};
});
// Handle tool calls
this.server.setRequestHandler(CallToolRequestSchema, async (request) => {
const { name, arguments: args } = request.params;
try {
const result = await this.executeTool(name, args || {});
return {
content: [
{
type: 'text',
text: JSON.stringify(result, null, 2),
},
],
};
} catch (error: any) {
return {
content: [
{
type: 'text',
text: JSON.stringify(
{
error: error.message,
code: error.code || 'UNKNOWN_ERROR',
},
null,
2
),
},
],
isError: true,
};
}
});
}
private getToolDefinitions(): Tool[] {
return [
{
name: 'list_vaults',
description: 'List all accessible Obsidian vaults',
inputSchema: {
type: 'object',
properties: {},
},
},
{
name: 'get_vault_info',
description: 'Get information about a specific vault',
inputSchema: {
type: 'object',
properties: {
vault: { type: 'string', description: 'Vault identifier (name or path)' },
},
required: ['vault'],
},
},
{
name: 'list_files',
description: 'List files in a vault',
inputSchema: {
type: 'object',
properties: {
vault: { type: 'string', description: 'Vault identifier' },
path: { type: 'string', description: 'Subpath to list (optional)' },
recursive: { type: 'boolean', description: 'List recursively (default: false)' },
includeHidden: { type: 'boolean', description: 'Include hidden files (default: false)' },
},
required: ['vault'],
},
},
{
name: 'get_file',
description: 'Read a file from a vault',
inputSchema: {
type: 'object',
properties: {
vault: { type: 'string', description: 'Vault identifier' },
filepath: { type: 'string', description: 'Path to the file' },
parseFrontmatter: { type: 'boolean', description: 'Parse YAML frontmatter (default: false)' },
},
required: ['vault', 'filepath'],
},
},
{
name: 'write_file',
description: 'Write or update a file in a vault',
inputSchema: {
type: 'object',
properties: {
vault: { type: 'string', description: 'Vault identifier' },
filepath: { type: 'string', description: 'Path to the file' },
content: { type: 'string', description: 'File content' },
frontmatter: { type: 'object', description: 'Optional YAML frontmatter' },
createDirs: { type: 'boolean', description: 'Create parent directories (default: true)' },
expectedModTime: { type: 'string', description: 'Expected modification time for conflict detection' },
},
required: ['vault', 'filepath', 'content'],
},
},
{
name: 'append_content',
description: 'Append content to an existing file',
inputSchema: {
type: 'object',
properties: {
vault: { type: 'string', description: 'Vault identifier' },
filepath: { type: 'string', description: 'Path to the file' },
content: { type: 'string', description: 'Content to append' },
},
required: ['vault', 'filepath', 'content'],
},
},
{
name: 'search_files',
description: 'Search for text in vault files',
inputSchema: {
type: 'object',
properties: {
vault: { type: 'string', description: 'Vault identifier' },
query: { type: 'string', description: 'Search query' },
path: { type: 'string', description: 'Subpath to search (optional)' },
caseSensitive: { type: 'boolean', description: 'Case sensitive search (default: false)' },
regex: { type: 'boolean', description: 'Use regex (default: false)' },
contextLength: { type: 'number', description: 'Context length around matches (default: 50)' },
},
required: ['vault', 'query'],
},
},
{
name: 'get_metadata',
description: 'Get metadata for a file (frontmatter, tags, links, word count)',
inputSchema: {
type: 'object',
properties: {
vault: { type: 'string', description: 'Vault identifier' },
filepath: { type: 'string', description: 'Path to the file' },
},
required: ['vault', 'filepath'],
},
},
{
name: 'execute_command',
description: 'Execute an Obsidian command (requires REST API plugin)',
inputSchema: {
type: 'object',
properties: {
vault: { type: 'string', description: 'Vault identifier' },
commandId: { type: 'string', description: 'Command ID' },
args: { type: 'object', description: 'Command arguments (optional)' },
},
required: ['vault', 'commandId'],
},
},
{
name: 'open_file',
description: 'Open a file in Obsidian UI (requires REST API plugin)',
inputSchema: {
type: 'object',
properties: {
vault: { type: 'string', description: 'Vault identifier' },
filepath: { type: 'string', description: 'Path to the file' },
},
required: ['vault', 'filepath'],
},
},
{
name: 'get_active_file',
description: 'Get the currently active file in Obsidian (requires REST API plugin)',
inputSchema: {
type: 'object',
properties: {
vault: { type: 'string', description: 'Vault identifier' },
},
required: ['vault'],
},
},
{
name: 'open_graph',
description: 'Open graph view in Obsidian (requires REST API plugin)',
inputSchema: {
type: 'object',
properties: {
vault: { type: 'string', description: 'Vault identifier' },
},
required: ['vault'],
},
},
];
}
private async executeTool(name: string, args: any): Promise<any> {
switch (name) {
case 'list_vaults':
return this.mcpTools.listVaults();
case 'get_vault_info':
return this.mcpTools.getVaultInfo(args.vault);
case 'list_files':
return this.mcpTools.listFiles(
args.vault,
args.path,
args.recursive,
args.includeHidden
);
case 'get_file':
return this.mcpTools.getFile(args.vault, args.filepath, args.parseFrontmatter);
case 'write_file':
return this.mcpTools.writeFile(
args.vault,
args.filepath,
args.content,
args.frontmatter,
args.createDirs,
args.expectedModTime
);
case 'append_content':
return this.mcpTools.appendContent(args.vault, args.filepath, args.content);
case 'search_files':
return this.mcpTools.searchFiles(
args.vault,
args.query,
args.path,
args.caseSensitive,
args.regex,
args.contextLength
);
case 'get_metadata':
return this.mcpTools.getMetadata(args.vault, args.filepath);
case 'execute_command':
return this.mcpTools.executeCommand(args.vault, args.commandId, args.args);
case 'open_file':
return this.mcpTools.openFile(args.vault, args.filepath);
case 'get_active_file':
return this.mcpTools.getActiveFile(args.vault);
case 'open_graph':
return this.mcpTools.openGraph(args.vault);
default:
throw new Error(`Unknown tool: ${name}`);
}
}
async start(): Promise<void> {
const transport = new StdioServerTransport();
await this.server.connect(transport);
console.error('MCP Obsidian Multi-Vault server running on stdio');
}
async stop(): Promise<void> {
this.lockManager.destroy();
await this.server.close();
}
}