/**
* MCP Server for Obsidian Local REST API
*/
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
import { allTools } from './tools/index.js';
import { checkHealth } from './utils/api-client.js';
import { getConfig, debugLog } from './utils/config.js';
import express, { Request, Response } from 'express';
import cors from 'cors';
/**
* Perform startup health check and log results
*/
async function performStartupHealthCheck(): Promise<boolean> {
const config = getConfig();
console.error('');
console.error('========================================');
console.error(' Obsidian MCP Server - Startup Check');
console.error('========================================');
console.error(`API URL: ${config.apiUrl}`);
console.error(`Debug Mode: ${config.debug ? 'enabled' : 'disabled'}`);
console.error('');
console.error('Checking connection to Obsidian REST API...');
const health = await checkHealth();
if (health.healthy && health.authenticated) {
console.error('✓ Connected to Obsidian REST API');
console.error(`✓ Authenticated successfully`);
if (health.version) {
console.error(` Plugin Version: ${health.version}`);
}
if (health.obsidianVersion) {
console.error(` Obsidian Version: ${health.obsidianVersion}`);
}
console.error('');
return true;
} else if (health.healthy && !health.authenticated) {
console.error('✓ Connected to Obsidian REST API');
console.error('✗ Authentication failed - check your API key');
console.error('');
return false;
} else {
console.error('✗ Failed to connect to Obsidian REST API');
if (health.error) {
console.error(` Error: ${health.error}`);
}
console.error('');
console.error('Troubleshooting:');
console.error(' 1. Ensure Obsidian is running');
console.error(' 2. Ensure the Local REST API plugin is enabled');
console.error(' 3. Check OBSIDIAN_API_URL in your .env file');
console.error(' 4. Check OBSIDIAN_API_KEY in your .env file');
console.error('');
return false;
}
}
/**
* Create and configure the MCP server
*/
export function createServer(): McpServer {
debugLog('Creating MCP server instance');
const server = new McpServer({
name: 'obsidian-mcp-server',
version: '1.0.0',
});
// Register all tools using the new registerTool() API
for (const tool of allTools) {
debugLog(`Registering tool: ${tool.name}`);
// Get the raw shape from Zod schema - this is what the SDK expects
const zodDef = (tool.inputSchema as any)._zod?.def || (tool.inputSchema as any)._def || {};
const rawShape = zodDef.shape || zodDef._shape || {};
server.registerTool(tool.name, {
description: tool.description,
inputSchema: rawShape
}, async (args: Record<string, unknown>) => {
debugLog(`Tool called: ${tool.name}`, args);
try {
// Validate input against schema
const validatedArgs = tool.inputSchema.parse(args);
const result = await tool.handler(validatedArgs);
debugLog(`Tool result: ${tool.name}`, result);
return {
content: [
{
type: 'text' as const,
text: result,
},
],
};
} catch (error) {
const errorMessage = error instanceof Error
? error.message
: 'Unknown error occurred';
debugLog(`Tool error: ${tool.name}`, errorMessage);
return {
content: [
{
type: 'text' as const,
text: `Error: ${errorMessage}`,
},
],
isError: true,
};
}
});
}
debugLog('All tools registered');
return server;
}
/**
* Start the MCP server with stdio transport
*/
export async function startStdioServer(): Promise<void> {
// Perform health check first
await performStartupHealthCheck();
const server = createServer();
const transport = new StdioServerTransport();
await server.connect(transport);
console.error('Obsidian MCP server started (stdio mode)');
console.error('Waiting for MCP messages on stdin...');
console.error('');
}
/**
* Start the MCP server with Streamable HTTP transport
* This is compatible with Open WebUI and other web-based MCP clients
*/
export async function startStreamableHTTPServer(port: number = 3000): Promise<void> {
// Perform health check first
await performStartupHealthCheck();
const app = express();
// Enable CORS for all origins (required for Open WebUI)
app.use(cors({
origin: '*',
methods: ['GET', 'POST', 'OPTIONS'],
allowedHeaders: ['Content-Type', 'Authorization', 'Accept'],
credentials: true
}));
// Parse JSON bodies
app.use(express.json());
// Handle OPTIONS preflight requests explicitly
app.options('/mcp', (_req: Request, res: Response) => {
debugLog('Handling OPTIONS preflight request');
res.status(200).send();
});
// Single endpoint handles all MCP requests (GET for SSE, POST for messages)
// Create NEW transport and server for EACH request (true stateless mode)
app.all('/mcp', async (req: Request, res: Response) => {
// Create fresh transport and server instances for each request
const transport = new StreamableHTTPServerTransport({
sessionIdGenerator: undefined, // Stateless mode
});
const server = createServer();
await server.connect(transport);
debugLog('Created new transport and server for request');
const config = getConfig();
if (config.debug) {
console.error(`[DEBUG] ========================================`);
console.error(`[DEBUG] Received ${req.method} request to /mcp`);
console.error(`[DEBUG] Headers:`, JSON.stringify(req.headers, null, 2));
console.error(`[DEBUG] Body:`, JSON.stringify(req.body, null, 2));
console.error(`[DEBUG] ========================================`);
} else {
debugLog(`Received ${req.method} request to /mcp`);
}
// Helper to decode chunk data to string (handles Buffer, Uint8Array, string)
const decodeChunk = (chunk: unknown): string => {
if (!chunk) return '';
if (Buffer.isBuffer(chunk)) {
return chunk.toString('utf8');
}
if (chunk instanceof Uint8Array) {
return Buffer.from(chunk).toString('utf8');
}
if (typeof chunk === 'string') {
return chunk;
}
// Fallback for other types
return String(chunk);
};
// Monkey-patch response methods to capture response data
const originalWrite = res.write.bind(res);
const originalEnd = res.end.bind(res);
let responseData = '';
res.write = function(chunk: unknown, encoding?: BufferEncoding | ((error: Error | null | undefined) => void), cb?: (error: Error | null | undefined) => void) {
responseData += decodeChunk(chunk);
return originalWrite(chunk, encoding as BufferEncoding, cb);
};
res.end = function(chunk?: unknown, encoding?: BufferEncoding | (() => void), cb?: () => void) {
responseData += decodeChunk(chunk);
if (config.debug) {
console.error(`[DEBUG] Response: ${res.statusCode} ${res.statusMessage || ''}`);
console.error(`[DEBUG] Content-Type: ${res.getHeader('content-type') || 'not set'}`);
// Pretty-print JSON if possible, otherwise show as-is
try {
// SSE responses have "data:" prefix, try to extract and format JSON
const jsonMatch = responseData.match(/data:\s*(\{.*\})/s);
if (jsonMatch) {
const parsed = JSON.parse(jsonMatch[1]);
console.error(`[DEBUG] Response Body:\n`, JSON.stringify(parsed, null, 2));
} else if (responseData) {
console.error(`[DEBUG] Response Body:`, responseData);
} else {
console.error(`[DEBUG] Response Body: (empty)`);
}
} catch {
console.error(`[DEBUG] Response Body:`, responseData || '(empty)');
}
}
return originalEnd(chunk, encoding as BufferEncoding, cb);
};
try {
await transport.handleRequest(req, res, req.body);
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
console.error(`[DEBUG] ERROR: ${errorMessage}`);
if (error instanceof Error && error.stack) {
console.error(`[DEBUG] Stack: ${error.stack}`);
}
if (!res.headersSent) {
res.status(500).json({ error: 'Internal server error', details: errorMessage });
}
}
});
// Health check endpoint
app.get('/health', async (_req: Request, res: Response) => {
const health = await checkHealth();
res.json({
status: health.healthy && health.authenticated ? 'ok' : 'degraded',
service: 'obsidian-mcp-server',
obsidian: {
connected: health.healthy,
authenticated: health.authenticated,
version: health.version,
obsidianVersion: health.obsidianVersion,
},
});
});
return new Promise((resolve) => {
app.listen(port, () => {
console.error(`Obsidian MCP server started (Streamable HTTP mode) on port ${port}`);
console.error(`MCP endpoint: http://localhost:${port}/mcp`);
console.error(`Health check: http://localhost:${port}/health`);
console.error('');
console.error('Configure Open WebUI with:');
console.error(` URL: http://localhost:${port}/mcp`);
console.error(' Type: MCP Streamable HTTP');
console.error(' Auth: None');
console.error('');
resolve();
});
});
}