diff --git a/src/adapters/file-manager-adapter.ts b/src/adapters/file-manager-adapter.ts index 0c4e9fd..6135ca1 100644 --- a/src/adapters/file-manager-adapter.ts +++ b/src/adapters/file-manager-adapter.ts @@ -1,5 +1,5 @@ import { FileManager, TAbstractFile, TFile } from 'obsidian'; -import { IFileManagerAdapter } from './interfaces'; +import { IFileManagerAdapter, FrontmatterValue } from './interfaces'; export class FileManagerAdapter implements IFileManagerAdapter { constructor(private fileManager: FileManager) {} @@ -12,7 +12,7 @@ export class FileManagerAdapter implements IFileManagerAdapter { await this.fileManager.trashFile(file); } - async processFrontMatter(file: TFile, fn: (frontmatter: any) => void): Promise { + async processFrontMatter(file: TFile, fn: (frontmatter: Record) => void): Promise { await this.fileManager.processFrontMatter(file, fn); } } \ No newline at end of file diff --git a/src/adapters/interfaces.ts b/src/adapters/interfaces.ts index cefd99c..abcd080 100644 --- a/src/adapters/interfaces.ts +++ b/src/adapters/interfaces.ts @@ -1,5 +1,10 @@ import { TAbstractFile, TFile, TFolder, CachedMetadata, DataWriteOptions } from 'obsidian'; +/** + * Frontmatter data structure (YAML-compatible types) + */ +export type FrontmatterValue = string | number | boolean | null | FrontmatterValue[] | { [key: string]: FrontmatterValue }; + /** * Adapter interface for Obsidian Vault operations */ @@ -56,5 +61,5 @@ export interface IFileManagerAdapter { // File operations renameFile(file: TAbstractFile, newPath: string): Promise; trashFile(file: TAbstractFile): Promise; - processFrontMatter(file: TFile, fn: (frontmatter: any) => void): Promise; + processFrontMatter(file: TFile, fn: (frontmatter: Record) => void): Promise; } \ No newline at end of file diff --git a/src/main.ts b/src/main.ts index eaeb2bc..d8487b7 100644 --- a/src/main.ts +++ b/src/main.ts @@ -24,7 +24,11 @@ export default class MCPServerPlugin extends Plugin { } // Migrate legacy settings (remove enableCORS and allowedOrigins) - const legacySettings = this.settings as any; + interface LegacySettings extends MCPPluginSettings { + enableCORS?: boolean; + allowedOrigins?: string[]; + } + const legacySettings = this.settings as LegacySettings; if ('enableCORS' in legacySettings || 'allowedOrigins' in legacySettings) { console.log('Migrating legacy CORS settings...'); delete legacySettings.enableCORS; diff --git a/src/server/mcp-server.ts b/src/server/mcp-server.ts index 6158eaa..cd71f93 100644 --- a/src/server/mcp-server.ts +++ b/src/server/mcp-server.ts @@ -4,6 +4,8 @@ import { Server } from 'http'; import { JSONRPCRequest, JSONRPCResponse, + JSONRPCParams, + JSONValue, ErrorCodes, InitializeResult, ListToolsResult, @@ -36,11 +38,11 @@ export class MCPServer { try { switch (request.method) { case 'initialize': - return this.createSuccessResponse(request.id, await this.handleInitialize(request.params)); + return this.createSuccessResponse(request.id, await this.handleInitialize(request.params ?? {})); case 'tools/list': return this.createSuccessResponse(request.id, await this.handleListTools()); case 'tools/call': - return this.createSuccessResponse(request.id, await this.handleCallTool(request.params)); + return this.createSuccessResponse(request.id, await this.handleCallTool(request.params ?? {})); case 'ping': return this.createSuccessResponse(request.id, {}); default: @@ -52,7 +54,7 @@ export class MCPServer { } } - private async handleInitialize(_params: any): Promise { + private async handleInitialize(_params: JSONRPCParams): Promise { return { protocolVersion: "2024-11-05", capabilities: { @@ -71,20 +73,20 @@ export class MCPServer { }; } - private async handleCallTool(params: any): Promise { - const { name, arguments: args } = params; - return await this.toolRegistry.callTool(name, args); + private async handleCallTool(params: JSONRPCParams): Promise { + const paramsObj = params as { name: string; arguments: Record }; + return await this.toolRegistry.callTool(paramsObj.name, paramsObj.arguments); } - private createSuccessResponse(id: string | number | undefined, result: any): JSONRPCResponse { + private createSuccessResponse(id: string | number | undefined, result: unknown): JSONRPCResponse { return { jsonrpc: "2.0", id: id ?? null, - result + result: result as JSONValue }; } - private createErrorResponse(id: string | number | undefined | null, code: number, message: string, data?: any): JSONRPCResponse { + private createErrorResponse(id: string | number | undefined | null, code: number, message: string, data?: JSONValue): JSONRPCResponse { return { jsonrpc: "2.0", id: id ?? null, @@ -104,7 +106,7 @@ export class MCPServer { resolve(); }); - this.server.on('error', (error: any) => { + this.server.on('error', (error: NodeJS.ErrnoException) => { if (error.code === 'EADDRINUSE') { reject(new Error(`Port ${this.settings.port} is already in use`)); } else { diff --git a/src/server/middleware.ts b/src/server/middleware.ts index dda6c88..234bc65 100644 --- a/src/server/middleware.ts +++ b/src/server/middleware.ts @@ -1,10 +1,10 @@ -import { Express, Request, Response } from 'express'; +import { Express, Request, Response, NextFunction } from 'express'; import express from 'express'; import cors from 'cors'; import { MCPServerSettings } from '../types/settings-types'; -import { ErrorCodes } from '../types/mcp-types'; +import { ErrorCodes, JSONRPCResponse } from '../types/mcp-types'; -export function setupMiddleware(app: Express, settings: MCPServerSettings, createErrorResponse: (id: any, code: number, message: string) => any): void { +export function setupMiddleware(app: Express, settings: MCPServerSettings, createErrorResponse: (id: string | number | null, code: number, message: string) => JSONRPCResponse): void { // Parse JSON bodies app.use(express.json()); @@ -29,7 +29,7 @@ export function setupMiddleware(app: Express, settings: MCPServerSettings, creat app.use(cors(corsOptions)); // Authentication middleware - Always enabled - app.use((req: Request, res: Response, next: any) => { + app.use((req: Request, res: Response, next: NextFunction) => { // Defensive check: if no API key is set, reject all requests if (!settings.apiKey || settings.apiKey.trim() === '') { return res.status(500).json(createErrorResponse(null, ErrorCodes.InternalError, 'Server misconfigured: No API key set')); @@ -45,7 +45,7 @@ export function setupMiddleware(app: Express, settings: MCPServerSettings, creat }); // Origin validation for security (DNS rebinding protection) - app.use((req: Request, res: Response, next: any) => { + app.use((req: Request, res: Response, next: NextFunction) => { const host = req.headers.host; // Only allow localhost connections diff --git a/src/server/routes.ts b/src/server/routes.ts index b924a4c..20e3a52 100644 --- a/src/server/routes.ts +++ b/src/server/routes.ts @@ -2,9 +2,9 @@ import { Express, Request, Response } from 'express'; import { JSONRPCRequest, JSONRPCResponse, ErrorCodes } from '../types/mcp-types'; export function setupRoutes( - app: Express, + app: Express, handleRequest: (request: JSONRPCRequest) => Promise, - createErrorResponse: (id: any, code: number, message: string) => JSONRPCResponse + createErrorResponse: (id: string | number | null, code: number, message: string) => JSONRPCResponse ): void { // Main MCP endpoint app.post('/mcp', async (req: Request, res: Response) => { diff --git a/src/tools/index.ts b/src/tools/index.ts index 6af000d..3445a32 100644 --- a/src/tools/index.ts +++ b/src/tools/index.ts @@ -474,6 +474,7 @@ export class ToolRegistry { ]; } + // eslint-disable-next-line @typescript-eslint/no-explicit-any async callTool(name: string, args: any): Promise { const startTime = Date.now(); diff --git a/src/tools/note-tools.ts b/src/tools/note-tools.ts index 852f19f..68a2c9e 100644 --- a/src/tools/note-tools.ts +++ b/src/tools/note-tools.ts @@ -13,7 +13,7 @@ import { } from '../types/mcp-types'; import { PathUtils } from '../utils/path-utils'; import { ErrorMessages } from '../utils/error-messages'; -import { FrontmatterUtils } from '../utils/frontmatter-utils'; +import { FrontmatterUtils, YAMLValue } from '../utils/frontmatter-utils'; import { WaypointUtils } from '../utils/waypoint-utils'; import { VersionUtils } from '../utils/version-utils'; import { ContentUtils } from '../utils/content-utils'; @@ -364,16 +364,28 @@ export class NoteTools { await this.vault.modify(file, content); // Build response with word count and link validation - const result: any = { + interface UpdateNoteResult { + success: boolean; + path: string; + versionId: string; + modified: number; + wordCount?: number; + linkValidation?: { + valid: string[]; + brokenNotes: Array<{ link: string; line: number; context: string }>; + brokenHeadings: Array<{ link: string; line: number; context: string; note: string }>; + summary: string; + }; + } + + const result: UpdateNoteResult = { success: true, path: file.path, versionId: VersionUtils.generateVersionId(file), - modified: file.stat.mtime + modified: file.stat.mtime, + wordCount: ContentUtils.countWords(content) }; - // Add word count - result.wordCount = ContentUtils.countWords(content); - // Add link validation if requested if (validateLinks) { result.linkValidation = await LinkUtils.validateLinks( @@ -731,7 +743,7 @@ export class NoteTools { */ async updateFrontmatter( path: string, - patch?: Record, + patch?: Record, remove: string[] = [], ifMatch?: string ): Promise { diff --git a/src/tools/vault-tools.ts b/src/tools/vault-tools.ts index ae192bc..56361e5 100644 --- a/src/tools/vault-tools.ts +++ b/src/tools/vault-tools.ts @@ -385,8 +385,10 @@ export class VaultTools { // In most cases, this will be 0 for directories let modified = 0; try { - if ((folder as any).stat && typeof (folder as any).stat.mtime === 'number') { - modified = (folder as any).stat.mtime; + // TFolder doesn't officially have stat, but it may exist in practice + const folderWithStat = folder as TFolder & { stat?: { mtime?: number } }; + if (folderWithStat.stat && typeof folderWithStat.stat.mtime === 'number') { + modified = folderWithStat.stat.mtime; } } catch (error) { // Silently fail - modified will remain 0 diff --git a/src/types/mcp-types.ts b/src/types/mcp-types.ts index ebcb0bb..7e5b62a 100644 --- a/src/types/mcp-types.ts +++ b/src/types/mcp-types.ts @@ -1,22 +1,39 @@ // MCP Protocol Types + +/** + * JSON-RPC compatible value types + */ +export type JSONValue = + | string + | number + | boolean + | null + | JSONValue[] + | { [key: string]: JSONValue }; + +/** + * JSON-RPC parameters can be an object or array + */ +export type JSONRPCParams = { [key: string]: JSONValue } | JSONValue[]; + export interface JSONRPCRequest { jsonrpc: "2.0"; id?: string | number; method: string; - params?: any; + params?: JSONRPCParams; } export interface JSONRPCResponse { jsonrpc: "2.0"; id: string | number | null; - result?: any; + result?: JSONValue; error?: JSONRPCError; } export interface JSONRPCError { code: number; message: string; - data?: any; + data?: JSONValue; } export enum ErrorCodes { @@ -38,12 +55,25 @@ export interface InitializeResult { }; } +/** + * JSON Schema property definition + */ +export interface JSONSchemaProperty { + type: string; + description?: string; + enum?: string[]; + items?: JSONSchemaProperty; + properties?: Record; + required?: string[]; + [key: string]: string | string[] | JSONSchemaProperty | Record | undefined; +} + export interface Tool { name: string; description: string; inputSchema: { type: string; - properties: Record; + properties: Record; required?: string[]; }; } @@ -160,7 +190,7 @@ export interface FrontmatterSummary { title?: string; tags?: string[]; aliases?: string[]; - [key: string]: any; + [key: string]: JSONValue | undefined; } export interface FileMetadataWithFrontmatter extends FileMetadata { @@ -179,7 +209,7 @@ export interface ParsedNote { path: string; hasFrontmatter: boolean; frontmatter?: string; - parsedFrontmatter?: Record; + parsedFrontmatter?: Record; content: string; contentWithoutFrontmatter?: string; wordCount?: number; @@ -200,9 +230,9 @@ export interface ExcalidrawMetadata { hasCompressedData?: boolean; /** Drawing metadata including appState and version */ metadata?: { - appState?: Record; + appState?: Record; version?: number; - [key: string]: any; + [key: string]: JSONValue | undefined; }; /** Preview text extracted from text elements section (when includePreview=true) */ preview?: string; diff --git a/src/ui/notifications.ts b/src/ui/notifications.ts index 34a702a..0dbab1c 100644 --- a/src/ui/notifications.ts +++ b/src/ui/notifications.ts @@ -7,6 +7,7 @@ import { MCPPluginSettings } from '../types/settings-types'; export interface NotificationHistoryEntry { timestamp: number; toolName: string; + // eslint-disable-next-line @typescript-eslint/no-explicit-any args: any; success: boolean; duration?: number; @@ -74,6 +75,7 @@ export class NotificationManager { /** * Show notification for tool call start */ + // eslint-disable-next-line @typescript-eslint/no-explicit-any showToolCall(toolName: string, args: any, duration?: number): void { if (!this.shouldShowNotification()) { return; @@ -140,6 +142,7 @@ export class NotificationManager { /** * Format arguments for display */ + // eslint-disable-next-line @typescript-eslint/no-explicit-any private formatArgs(args: any): string { if (!this.settings.showParameters) { return ''; diff --git a/src/utils/encryption-utils.ts b/src/utils/encryption-utils.ts index 39c8c4e..f729acc 100644 --- a/src/utils/encryption-utils.ts +++ b/src/utils/encryption-utils.ts @@ -1,5 +1,12 @@ +// Define Electron SafeStorage interface +interface ElectronSafeStorage { + isEncryptionAvailable(): boolean; + encryptString(plainText: string): Buffer; + decryptString(encrypted: Buffer): string; +} + // Safely import safeStorage - may not be available in all environments -let safeStorage: any = null; +let safeStorage: ElectronSafeStorage | null = null; try { const electron = require('electron'); safeStorage = electron.safeStorage || null; @@ -35,7 +42,7 @@ export function encryptApiKey(apiKey: string): string { } try { - const encrypted = safeStorage.encryptString(apiKey); + const encrypted = safeStorage!.encryptString(apiKey); return `encrypted:${encrypted.toString('base64')}`; } catch (error) { console.error('Failed to encrypt API key, falling back to plaintext:', error); diff --git a/src/utils/frontmatter-utils.ts b/src/utils/frontmatter-utils.ts index a1cf135..1eb6dd1 100644 --- a/src/utils/frontmatter-utils.ts +++ b/src/utils/frontmatter-utils.ts @@ -1,5 +1,16 @@ import { parseYaml } from 'obsidian'; +/** + * YAML value types that can appear in frontmatter + */ +export type YAMLValue = + | string + | number + | boolean + | null + | YAMLValue[] + | { [key: string]: YAMLValue }; + /** * Utility class for parsing and extracting frontmatter from markdown files */ @@ -11,7 +22,7 @@ export class FrontmatterUtils { static extractFrontmatter(content: string): { hasFrontmatter: boolean; frontmatter: string; - parsedFrontmatter: Record | null; + parsedFrontmatter: Record | null; content: string; contentWithoutFrontmatter: string; } { @@ -59,7 +70,7 @@ export class FrontmatterUtils { const contentWithoutFrontmatter = contentLines.join('\n'); // Parse YAML using Obsidian's built-in parser - let parsedFrontmatter: Record | null = null; + let parsedFrontmatter: Record | null = null; try { parsedFrontmatter = parseYaml(frontmatter) || {}; } catch (error) { @@ -80,17 +91,17 @@ export class FrontmatterUtils { * Extract only the frontmatter summary (common fields) * Useful for list operations without reading full content */ - static extractFrontmatterSummary(parsedFrontmatter: Record | null): { + static extractFrontmatterSummary(parsedFrontmatter: Record | null): { title?: string; tags?: string[]; aliases?: string[]; - [key: string]: any; + [key: string]: YAMLValue | undefined; } | null { if (!parsedFrontmatter) { return null; } - const summary: Record = {}; + const summary: Record = {}; // Extract common fields if (parsedFrontmatter.title) { @@ -136,7 +147,7 @@ export class FrontmatterUtils { * Serialize frontmatter object to YAML string with delimiters * Returns the complete frontmatter block including --- delimiters */ - static serializeFrontmatter(data: Record): string { + static serializeFrontmatter(data: Record): string { if (!data || Object.keys(data).length === 0) { return ''; } @@ -203,7 +214,7 @@ export class FrontmatterUtils { isExcalidraw: boolean; elementCount?: number; hasCompressedData?: boolean; - metadata?: Record; + metadata?: Record; } { try { // Excalidraw files are typically markdown with a code block containing JSON @@ -287,7 +298,7 @@ export class FrontmatterUtils { // Check if data is compressed (base64 encoded) const trimmedJson = jsonString.trim(); - let jsonData: any; + let jsonData: Record; if (trimmedJson.startsWith('N4KAk') || !trimmedJson.startsWith('{')) { // Data is compressed - try to decompress @@ -328,9 +339,9 @@ export class FrontmatterUtils { // Parse the JSON (uncompressed format) jsonData = JSON.parse(trimmedJson); - + // Count elements - const elementCount = jsonData.elements ? jsonData.elements.length : 0; + const elementCount = Array.isArray(jsonData.elements) ? jsonData.elements.length : 0; // Check for compressed data (files or images) const hasCompressedData = !!(jsonData.files && Object.keys(jsonData.files).length > 0);