Obsidian MCP Server
by cyanheads
Verified
/**
* MCP server request handlers
*/
import {
Tool,
TextContent,
ListToolsRequestSchema,
CallToolRequestSchema,
ListResourcesRequestSchema,
ReadResourceRequestSchema
} from "@modelcontextprotocol/sdk/types.js";
import { Server } from "@modelcontextprotocol/sdk/server/index.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,
createFailureResult,
createSuccessResult
} from "./types.js";
import { BaseToolHandler } from "../tools/base.js";
// Create a logger for request handlers
const logger = createLogger('McpHandlers');
/**
* Helper function to safely mask sensitive data
*/
function maskSensitiveData(data: Record<string, unknown> | undefined): Record<string, unknown> {
if (!data) return {};
const sensitiveFields = ['password', 'token', 'secret', 'key', 'auth', 'credential'];
const result: Record<string, unknown> = {};
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 as Record<string, unknown>);
} 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: Server,
toolHandlers: Map<string, BaseToolHandler<any>>
): void {
server.setRequestHandler(ListToolsRequestSchema, async () => {
logger.debug('Handling ListToolsRequest');
// Start performance timing
logger.startTimer('list_tools');
try {
const tools: Tool[] = [];
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: Server,
toolHandlers: Map<string, BaseToolHandler<any>>
): void {
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<never>((_, 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: Server,
resources: Record<string, any>
): void {
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: Server,
resources: Record<string, any>
): void {
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 }
);
}
});
}