import { createConnection } from 'net';
import { dirname, basename } from 'path';
/**
* Creates a connection to the MCP server
* This connector is used by Claude Desktop to communicate with our Obsidian plugin
* Uses named pipes on Windows and Unix domain sockets on macOS/Linux
*
* The IPC path now includes the vault name to support multiple vaults
*/
/**
* Sanitizes a vault name for use in identifiers, filenames, and configuration keys
*
* This function standardizes vault names by:
* - Converting to lowercase
* - Removing special characters (keeping only alphanumeric, spaces, and hyphens)
* - Replacing spaces with hyphens
* - Normalizing multiple consecutive hyphens to a single hyphen
*
* @param vaultName - The original vault name to sanitize
* @returns A sanitized version of the vault name suitable for use in identifiers
*/
const sanitizeVaultName = (vaultName) => {
if (!vaultName) return '';
return vaultName
.toLowerCase() // Convert to lowercase
.replace(/[^\w\s-]/g, '') // Remove special characters (keep alphanumeric, spaces, hyphens)
.replace(/\s+/g, '-') // Replace spaces with hyphens
.replace(/-+/g, '-'); // Replace multiple consecutive hyphens with a single one
};
/**
* Extracts the vault name from the script execution path
*
* The script path follows the pattern:
* /path/to/vault_name/.obsidian/plugins/nexus/connector.js
*
* We need to go up 4 levels from the script path to reach the vault name:
* 1. dirname(scriptPath) -> /path/to/vault_name/.obsidian/plugins/nexus
* 2. dirname() -> /path/to/vault_name/.obsidian/plugins
* 3. dirname() -> /path/to/vault_name/.obsidian
* 4. dirname() -> /path/to/vault_name
* 5. basename() -> vault_name
*
* @returns The extracted vault name or empty string if not found
*/
const extractVaultName = () => {
try {
// Get the script path from process.argv
const scriptPath = process.argv[1];
if (!scriptPath) {
return '';
}
// Go up 4 levels in the directory hierarchy to reach the vault name
// 1. nexus plugin directory
const pluginDir = dirname(scriptPath);
// 2. plugins directory
const pluginsDir = dirname(pluginDir);
// 3. .obsidian directory
const obsidianDir = dirname(pluginsDir);
// 4. vault directory (parent of .obsidian)
const vaultDir = dirname(obsidianDir);
// The vault name is the basename of the vault directory
const vaultName = basename(vaultDir);
return vaultName || '';
} catch (error) {
// Silent failure - will use empty vault name
return '';
}
};
/**
* Gets the IPC path with vault name included
*
* This creates a unique IPC path for each vault to prevent conflicts
* between different vault instances.
*
* @returns The IPC path string with vault name included
*/
const getIPCPath = () => {
// Extract and sanitize the vault name
const vaultName = extractVaultName();
const sanitizedVaultName = sanitizeVaultName(vaultName);
// Add the sanitized vault name to the IPC path
return process.platform === 'win32'
? `\\\\.\\pipe\\nexus_mcp_${sanitizedVaultName}`
: `/tmp/nexus_mcp_${sanitizedVaultName}.sock`;
};
// Retry configuration
const MAX_BACKOFF_MS = 30000; // Cap backoff at 30 seconds
let retryCount = 0;
/**
* Calculates the backoff delay using exponential backoff with a cap
*
* @param attempt - The current retry attempt number (0-indexed)
* @returns The delay in milliseconds (capped at MAX_BACKOFF_MS)
*/
function calculateBackoff(attempt: number): number {
// Exponential backoff: 1s, 2s, 4s, 8s, 16s, 30s, 30s, ...
const exponentialDelay = 1000 * Math.pow(2, attempt);
return Math.min(MAX_BACKOFF_MS, exponentialDelay);
}
/**
* Attempts to connect to the MCP server with infinite retry logic
*
* This function:
* 1. Creates a connection to the IPC path
* 2. Sets up error handling (completely silent for normal "waiting" errors)
* 3. Implements infinite retry with capped exponential backoff (max 30s)
* 4. Automatically connects when Obsidian becomes available
*
* Note: This connector is completely silent during normal operation to avoid
* showing error notifications in Claude Desktop. stderr output is treated as
* error notifications by Claude Desktop.
*/
function connectWithRetry() {
const ipcPath = getIPCPath();
// Track whether this socket ever successfully connected
let hasConnected = false;
try {
const socket = createConnection(ipcPath);
// Pipe stdin/stdout to/from the socket
process.stdin.pipe(socket);
socket.pipe(process.stdout);
// Error handling - completely silent for expected connection errors
socket.on('error', (err) => {
// Cast error to NodeJS.ErrnoException to access the code property
const nodeErr = err as NodeJS.ErrnoException;
const isWaitingError = nodeErr.code === 'ENOENT' || nodeErr.code === 'ECONNREFUSED';
// Be completely silent - any stderr output shows as notification in Claude Desktop
// Only truly unexpected errors should be logged (never ENOENT/ECONNREFUSED)
if (!isWaitingError) {
// Even unexpected errors should be silent - user can't fix them anyway
// and it would clutter Claude Desktop with notifications
}
// Always retry with exponential backoff (capped at 30s)
retryCount++;
const retryDelay = calculateBackoff(retryCount);
setTimeout(connectWithRetry, retryDelay);
});
socket.on('connect', () => {
hasConnected = true;
// Silent on connection - no need to notify user
// Reset retry count on successful connection
retryCount = 0;
});
socket.on('close', () => {
if (hasConnected) {
// Connection was lost - silently go back to waiting mode
retryCount = 0; // Reset backoff
setTimeout(connectWithRetry, 1000); // Start retry loop
}
// If we never connected, the error handler will schedule a retry
});
} catch (error) {
// Only exit on catastrophic errors (shouldn't happen in normal operation)
// Be silent even here - user sees MCP server as "disconnected" in Claude Desktop
process.exit(1);
}
}
// Start the connection process
connectWithRetry();