#!/usr/bin/env node
// ABOUTME: Main entry point for the Obsidian MCP server
// ABOUTME: Supports both stdio and HTTP transport modes via MCP_TRANSPORT env var
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
import {
CallToolRequestSchema,
ListToolsRequestSchema,
ListResourcesRequestSchema,
ReadResourceRequestSchema,
} from "@modelcontextprotocol/sdk/types.js";
import { createServer, IncomingMessage, ServerResponse } from "node:http";
import { randomUUID } from "node:crypto";
import { tools, handleToolCall } from "./tools/index.js";
import { getVaultPath, listRecentNotes } from "./utils/vault.js";
function createMcpServer(): Server {
const server = new Server(
{
name: "obsidian-mcp",
version: "1.0.0",
},
{
capabilities: {
tools: {},
resources: {},
},
}
);
// List available tools
server.setRequestHandler(ListToolsRequestSchema, async () => {
return { tools };
});
// Handle tool calls
server.setRequestHandler(CallToolRequestSchema, async (request) => {
const { name, arguments: args } = request.params;
return handleToolCall(name, args);
});
// List resources (recent notes as resources)
server.setRequestHandler(ListResourcesRequestSchema, async () => {
const vaultPath = getVaultPath();
const recentNotes = await listRecentNotes(vaultPath, 20);
return {
resources: recentNotes.map((note) => ({
uri: `obsidian://note/${encodeURIComponent(note.relativePath)}`,
name: note.title,
description: `Last modified: ${note.modified.toISOString()}`,
mimeType: "text/markdown",
})),
};
});
// Read resource content
server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
const { uri } = request.params;
const match = uri.match(/^obsidian:\/\/note\/(.+)$/);
if (!match) {
throw new Error(`Invalid resource URI: ${uri}`);
}
const relativePath = decodeURIComponent(match[1]);
const vaultPath = getVaultPath();
const fullPath = `${vaultPath}/${relativePath}`;
const fs = await import("fs/promises");
const content = await fs.readFile(fullPath, "utf-8");
return {
contents: [
{
uri,
mimeType: "text/markdown",
text: content,
},
],
};
});
return server;
}
async function startStdioTransport(server: Server): Promise<void> {
const transport = new StdioServerTransport();
await server.connect(transport);
console.error("Obsidian MCP server running on stdio");
}
interface SessionData {
transport: StreamableHTTPServerTransport;
server: Server;
}
async function startHttpTransport(_unusedServer: Server): Promise<void> {
const port = parseInt(process.env.MCP_HTTP_PORT || "3000", 10);
// Track active sessions with their own server and transport instances
const sessions = new Map<string, SessionData>();
const httpServer = createServer(async (req: IncomingMessage, res: ServerResponse) => {
const url = new URL(req.url || "/", `http://${req.headers.host}`);
// Health check endpoint
if (url.pathname === "/health") {
res.writeHead(200, { "Content-Type": "application/json" });
res.end(JSON.stringify({ status: "ok", transport: "http" }));
return;
}
// MCP endpoint
if (url.pathname === "/mcp") {
// Handle session-based routing for existing sessions
const sessionId = req.headers["mcp-session-id"] as string | undefined;
if (sessionId && sessions.has(sessionId)) {
const sessionData = sessions.get(sessionId)!;
await sessionData.transport.handleRequest(req, res);
return;
}
// For new connections or initialization requests, create a new server and transport
if (req.method === "POST" || req.method === "GET") {
// Create a fresh server instance for this session
const sessionServer = createMcpServer();
const transport = new StreamableHTTPServerTransport({
sessionIdGenerator: () => randomUUID(),
onsessioninitialized: (id) => {
sessions.set(id, { transport, server: sessionServer });
console.error(`Session initialized: ${id}`);
},
onsessionclosed: (id) => {
sessions.delete(id);
console.error(`Session closed: ${id}`);
},
});
transport.onclose = () => {
if (transport.sessionId) {
sessions.delete(transport.sessionId);
}
};
// Connect this session's server to its transport
await sessionServer.connect(transport);
await transport.handleRequest(req, res);
return;
}
// Method not allowed
res.writeHead(405, { "Content-Type": "application/json" });
res.end(JSON.stringify({ error: "Method not allowed" }));
return;
}
// Not found
res.writeHead(404, { "Content-Type": "application/json" });
res.end(JSON.stringify({ error: "Not found" }));
});
httpServer.listen(port, () => {
console.error(`Obsidian MCP server running on http://0.0.0.0:${port}/mcp`);
console.error(`Health check available at http://0.0.0.0:${port}/health`);
});
// Handle graceful shutdown
process.on("SIGTERM", () => {
console.error("Received SIGTERM, shutting down...");
httpServer.close();
process.exit(0);
});
process.on("SIGINT", () => {
console.error("Received SIGINT, shutting down...");
httpServer.close();
process.exit(0);
});
}
async function main() {
const server = createMcpServer();
const transport = process.env.MCP_TRANSPORT || "stdio";
if (transport === "http") {
await startHttpTransport(server);
} else {
await startStdioTransport(server);
}
}
main().catch((error) => {
console.error("Fatal error:", error);
process.exit(1);
});