obsidian-mcp-server
Version:
Model Context Protocol (MCP) server designed for LLMs to interact with Obsidian vaults. Provides secure, token-aware tools for seamless knowledge base management through a standardized interface.
287 lines • 12.9 kB
JavaScript
/**
* MCP server request handlers
*/
import { ListToolsRequestSchema, CallToolRequestSchema, ListResourcesRequestSchema, ReadResourceRequestSchema } from "@modelcontextprotocol/sdk/types.js";
import { ObsidianError } from "../utils/errors.js";
import { validateToolArguments } from "../utils/validation.js";
import { rateLimiter } from "../utils/rate-limiting.js";
import { createLogger, ErrorCategoryType } from "../utils/logging.js";
import { DEFAULT_TIMEOUT_CONFIG, McpErrorCode } from "./types.js";
// Create a logger for request handlers
const logger = createLogger('McpHandlers');
/**
* Helper function to safely mask sensitive data
*/
function maskSensitiveData(data) {
if (!data)
return {};
const sensitiveFields = ['password', 'token', 'secret', 'key', 'auth', 'credential'];
const result = {};
for (const [key, value] of Object.entries(data)) {
const isSensitive = sensitiveFields.some(field => key.toLowerCase().includes(field.toLowerCase()));
if (isSensitive) {
result[key] = '********';
}
else if (value && typeof value === 'object' && !Array.isArray(value)) {
result[key] = maskSensitiveData(value);
}
else {
result[key] = value;
}
}
return result;
}
/**
* Set up tool listing handler
* @param server The MCP server instance
* @param toolHandlers The tool handlers to register
*/
export function setupToolListingHandler(server, toolHandlers) {
server.setRequestHandler(ListToolsRequestSchema, async () => {
logger.debug('Handling ListToolsRequest');
// Start performance timing
logger.startTimer('list_tools');
try {
const tools = [];
for (const handler of toolHandlers.values()) {
tools.push(handler.getToolDescription());
}
// Log success and timing information
const elapsedMs = logger.endTimer('list_tools', 'Listed tools');
logger.logOperationResult(true, 'list_tools', elapsedMs, {
toolCount: tools.length
});
return { tools };
}
catch (error) {
// Log failure with timing information
const elapsedMs = logger.endTimer('list_tools', 'Failed to list tools');
logger.logOperationResult(false, 'list_tools', elapsedMs);
logger.error('Failed to list available tools', error instanceof Error ? error : undefined);
throw error;
}
});
}
/**
* Set up tool calling handler
* @param server The MCP server instance
* @param toolHandlers The tool handlers to register
*/
export function setupToolCallingHandler(server, toolHandlers) {
server.setRequestHandler(CallToolRequestSchema, async (request) => {
const { name, arguments: args } = request.params;
const operationId = `call_tool_${name}_${Date.now()}`;
logger.debug(`Handling CallToolRequest for tool: ${name}`, {
toolName: name,
operationId
});
// Start performance timing
logger.startTimer(operationId);
// Handle unknown tool
const handler = toolHandlers.get(name);
if (!handler) {
const errorInfo = {
toolName: name,
errorCode: McpErrorCode.NOT_FOUND,
errorCategory: ErrorCategoryType.CATEGORY_VALIDATION
};
logger.error(`Unknown tool requested: ${name}`, errorInfo);
const elapsedMs = logger.endTimer(operationId);
logger.logOperationResult(false, 'call_tool', elapsedMs, errorInfo);
throw new ObsidianError(`Unknown tool: ${name}`, McpErrorCode.NOT_FOUND);
}
// Check rate limit
try {
rateLimiter.enforceRateLimit(name);
}
catch (error) {
const errorInfo = {
toolName: name,
errorCode: McpErrorCode.RATE_LIMIT_EXCEEDED,
errorCategory: ErrorCategoryType.CATEGORY_SYSTEM
};
logger.warn(`Rate limit exceeded for tool: ${name}`, errorInfo);
const elapsedMs = logger.endTimer(operationId);
logger.logOperationResult(false, 'call_tool', elapsedMs, errorInfo);
throw new ObsidianError(`Rate limit exceeded for tool: ${name}`, McpErrorCode.RATE_LIMIT_EXCEEDED);
}
// Add timeout handling
const timeoutMs = DEFAULT_TIMEOUT_CONFIG.toolExecutionMs;
const timeoutPromise = new Promise((_, reject) => {
setTimeout(() => {
const errorInfo = {
toolName: name,
timeoutMs,
errorCode: McpErrorCode.TIMEOUT,
errorCategory: ErrorCategoryType.CATEGORY_SYSTEM
};
logger.error(`Tool execution timed out after ${timeoutMs}ms`, errorInfo);
reject(new ObsidianError(`Tool execution timed out after ${timeoutMs}ms`, McpErrorCode.TIMEOUT));
}, timeoutMs);
});
try {
// Validate arguments against tool's schema
const toolDescription = handler.getToolDescription();
const validationResult = validateToolArguments(args, toolDescription.inputSchema);
if (!validationResult.valid) {
const errorInfo = {
toolName: name,
validationErrors: validationResult.errors,
providedArgs: maskSensitiveData(args),
errorCode: McpErrorCode.BAD_REQUEST,
errorCategory: ErrorCategoryType.CATEGORY_VALIDATION
};
logger.error(`Invalid tool arguments for ${name}:`, errorInfo);
const elapsedMs = logger.endTimer(operationId);
logger.logOperationResult(false, 'call_tool', elapsedMs, errorInfo);
throw new ObsidianError(`Invalid tool arguments: ${validationResult.errors.join(', ')}`, McpErrorCode.BAD_REQUEST);
}
// Log the tool execution
logger.info(`Executing tool: ${name}`, {
toolName: name,
args: maskSensitiveData(args)
});
// Race between tool execution and timeout
const content = await Promise.race([
handler.runTool(args),
timeoutPromise
]);
// Log successful execution
const elapsedMs = logger.endTimer(operationId);
logger.logOperationResult(true, 'call_tool', elapsedMs, {
toolName: name,
contentLength: content.reduce((sum, item) => {
return sum + (item.type === 'text' ? item.text.length : 0);
}, 0)
});
return { content };
}
catch (error) {
// Handle ObsidianError
if (error instanceof ObsidianError) {
// Check if the operation actually succeeded despite the error
if (error.errorCode === McpErrorCode.SUCCESS_NO_CONTENT) {
const elapsedMs = logger.endTimer(operationId);
logger.logOperationResult(true, 'call_tool', elapsedMs, {
toolName: name,
status: 'success_no_content'
});
return {
content: [{
type: "text",
text: "Operation completed successfully"
}]
};
}
// Log failure for other ObsidianErrors
const errorInfo = {
toolName: name,
errorMessage: error.message,
errorCode: error.errorCode,
errorCategory: ErrorCategoryType.CATEGORY_BUSINESS_LOGIC,
details: error.details ? JSON.stringify(error.details) : undefined
};
const elapsedMs = logger.endTimer(operationId);
logger.logOperationResult(false, 'call_tool', elapsedMs, errorInfo);
throw error;
}
// Enhanced error logging for other errors
const errorMessage = error instanceof Error ? error.message : String(error);
const errorStack = error instanceof Error ? error.stack : undefined;
const errorInfo = {
toolName: name,
errorMessage,
errorStack,
errorCategory: ErrorCategoryType.CATEGORY_SYSTEM
};
logger.error("Tool execution error:", errorInfo);
const elapsedMs = logger.endTimer(operationId);
logger.logOperationResult(false, 'call_tool', elapsedMs, errorInfo);
throw new ObsidianError(`Tool '${name}' execution failed: ${errorMessage}`, McpErrorCode.INTERNAL_SERVER_ERROR, { originalError: errorMessage, stack: errorStack });
}
});
}
/**
* Set up resource listing handler
* @param server The MCP server instance
* @param resources The resources to register
*/
export function setupResourceListingHandler(server, resources) {
server.setRequestHandler(ListResourcesRequestSchema, async () => {
logger.debug('Handling ListResourcesRequest');
// Start performance timing
logger.startTimer('list_resources');
try {
const resourceList = Object.values(resources).map(resource => resource.getResourceDescription());
// Log success with timing information
const elapsedMs = logger.endTimer('list_resources', 'Listed resources');
logger.logOperationResult(true, 'list_resources', elapsedMs, {
resourceCount: resourceList.length
});
return { resources: resourceList };
}
catch (error) {
// Log failure with timing information
const elapsedMs = logger.endTimer('list_resources', 'Failed to list resources');
logger.logOperationResult(false, 'list_resources', elapsedMs);
logger.error('Failed to list available resources', error instanceof Error ? error : undefined);
throw error;
}
});
}
/**
* Set up resource reading handler
* @param server The MCP server instance
* @param resources The resources to register
*/
export function setupResourceReadingHandler(server, resources) {
server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
const uri = request.params.uri;
const operationId = `read_resource_${Date.now()}`;
logger.debug(`Handling ReadResourceRequest for URI: ${uri}`, {
resourceUri: uri,
operationId
});
// Start performance timing
logger.startTimer(operationId);
const resource = resources[uri];
if (!resource) {
const errorInfo = {
resourceUri: uri,
errorCode: McpErrorCode.NOT_FOUND,
errorCategory: ErrorCategoryType.CATEGORY_DATA_ACCESS
};
logger.error(`Resource not found: ${uri}`, errorInfo);
const elapsedMs = logger.endTimer(operationId);
logger.logOperationResult(false, 'read_resource', elapsedMs, errorInfo);
throw new ObsidianError(`Resource not found: ${uri}`, McpErrorCode.NOT_FOUND);
}
try {
logger.debug(`Found resource for URI: ${uri}`);
const contents = await resource.getContent();
// Log success with timing information
const elapsedMs = logger.endTimer(operationId);
logger.logOperationResult(true, 'read_resource', elapsedMs, {
resourceUri: uri,
contentItems: contents.length
});
return { contents };
}
catch (error) {
// Log failure with timing information
const errorMessage = error instanceof Error ? error.message : String(error);
const errorStack = error instanceof Error ? error.stack : undefined;
const errorInfo = {
resourceUri: uri,
errorMessage,
errorStack,
errorCategory: ErrorCategoryType.CATEGORY_DATA_ACCESS
};
logger.error(`Error reading resource: ${uri}`, errorInfo);
const elapsedMs = logger.endTimer(operationId);
logger.logOperationResult(false, 'read_resource', elapsedMs, errorInfo);
throw new ObsidianError(`Failed to read resource: ${errorMessage}`, McpErrorCode.INTERNAL_SERVER_ERROR, { originalError: errorMessage, stack: errorStack });
}
});
}
//# sourceMappingURL=handlers.js.map