diff --git a/README.md b/README.md index 102a0e8..df9ee9f 100644 --- a/README.md +++ b/README.md @@ -41,39 +41,30 @@ Traditional file access gives AI a narrow view - one document at a time. This pl ### 2. Configure Your AI Client -**For Claude Desktop / Claude Code** -```json -{ - "mcpServers": { - "obsidian-vault": { - "command": "npx", - "args": ["mcp-remote", "http://localhost:3001/mcp"] - } - } -} +**Claude Code** +```bash +claude mcp add --transport http obsidian http://localhost:3001/mcp --header "Authorization: Bearer YOUR_API_KEY" ``` -**With Authentication** (if enabled in plugin settings) +**Claude Desktop, Cline, and other MCP clients** ```json { "mcpServers": { "obsidian-vault": { - "command": "npx", - "args": [ - "mcp-remote", - "https://localhost:3443/mcp", - "--header", - "Authorization:${AUTH}" - ], - "env": { - "NODE_TLS_REJECT_UNAUTHORIZED": "0", - "AUTH": "Bearer YOUR_API_KEY" + "transport": { + "type": "http", + "url": "http://localhost:3001/mcp", + "headers": { + "Authorization": "Bearer YOUR_API_KEY" + } } } } } ``` +Copy the ready-to-use config with your API key from the plugin settings page. + ### 3. Start Using Once connected, simply chat with your AI assistant about your notes! For example: diff --git a/docs/architecture/INDEX.md b/docs/architecture/INDEX.md index ad9896a..4f01bd9 100644 --- a/docs/architecture/INDEX.md +++ b/docs/architecture/INDEX.md @@ -20,4 +20,4 @@ _Server architecture, transport, connection handling, plugin lifecycle_ | ADR | Title | Status | |-----|-------|--------| -| [ADR-100](./core/ADR-100-remove-concurrent-mode-toggle-and-simplify-connection-setup.md) | Remove concurrent mode toggle and simplify connection setup | Draft | +| [ADR-100](./core/ADR-100-remove-concurrent-mode-toggle-and-simplify-connection-setup.md) | Remove concurrent mode toggle and simplify connection setup | Accepted | diff --git a/docs/architecture/core/ADR-100-remove-concurrent-mode-toggle-and-simplify-connection-setup.md b/docs/architecture/core/ADR-100-remove-concurrent-mode-toggle-and-simplify-connection-setup.md index 1c95f70..f15a533 100644 --- a/docs/architecture/core/ADR-100-remove-concurrent-mode-toggle-and-simplify-connection-setup.md +++ b/docs/architecture/core/ADR-100-remove-concurrent-mode-toggle-and-simplify-connection-setup.md @@ -1,5 +1,5 @@ --- -status: Draft +status: Accepted date: 2026-03-14 deciders: - aaronsb diff --git a/docs/troubleshooting.md b/docs/troubleshooting.md index e471279..db2a8d9 100644 --- a/docs/troubleshooting.md +++ b/docs/troubleshooting.md @@ -2,58 +2,6 @@ Common issues and solutions for the Obsidian MCP Plugin. -## Windows: "Program is not recognized" Error - -**Symptoms:** -When connecting Claude Desktop to the MCP server on Windows, you may see: -``` -'D:\Program' is not recognized as an internal or external command, operable program or batch file. -``` - -**Cause:** -This occurs when Node.js is installed in a path containing spaces (e.g., `D:\Program Files\nodejs\`). Claude Desktop resolves `npx` to its full path but doesn't properly quote it when spawning the process, causing Windows to interpret `D:\Program` as the command. - -**Solution:** -Use the full path to `npx.cmd` from your npm global directory instead of just `npx`: - -```json -{ - "mcpServers": { - "obsidian-vault": { - "command": "C:\\Users\\YOUR_USERNAME\\AppData\\Roaming\\npm\\npx.cmd", - "args": ["mcp-remote", "http://localhost:3001/mcp"] - } - } -} -``` - -**With Authentication:** -```json -{ - "mcpServers": { - "obsidian-vault": { - "command": "C:\\Users\\YOUR_USERNAME\\AppData\\Roaming\\npm\\npx.cmd", - "args": [ - "mcp-remote", - "https://localhost:3443/mcp", - "--header", - "Authorization:${AUTH}" - ], - "env": { - "NODE_TLS_REJECT_UNAUTHORIZED": "0", - "AUTH": "Bearer YOUR_API_KEY" - } - } - } -} -``` - -**Alternative Solutions:** -1. **DOS 8.3 short names**: Use `D:\PROGRA~1\nodejs\npx.cmd` (varies by system) -2. **Reinstall Node.js** to a path without spaces (e.g., `C:\nodejs\`) - -**Note:** This is a known issue with Claude Desktop's handling of Windows paths. The npm global directory solution is the most reliable workaround. - ## Connection Refused **Symptoms:** @@ -73,7 +21,7 @@ Connection works but requests are rejected with 401/403 errors. **Solutions:** 1. **Check API key**: Ensure the key in your client config matches the one in plugin settings 2. **Header format**: Use `Authorization: Bearer YOUR_KEY` (note the space after Bearer) -3. **HTTPS required**: Authentication only works over HTTPS (port 3443 by default) +3. **Regenerated key**: The API key regenerates on plugin updates — copy the new key from settings ## SSL Certificate Errors diff --git a/src/main.ts b/src/main.ts index b487027..e4ed8c2 100644 --- a/src/main.ts +++ b/src/main.ts @@ -8,19 +8,7 @@ import { PluginDetector } from './utils/plugin-detector'; import { CertificateConfig } from './utils/certificate-manager'; import { ValidationConfig } from './validation/input-validator'; -interface MCPServerEntry { - command?: string; - args?: string[]; - env?: Record; - transport?: { - type: string; - url: string; - }; -} -interface MCPClientConfig { - mcpServers: Record; -} interface MCPPluginSettings { httpEnabled: boolean; @@ -31,8 +19,6 @@ interface MCPPluginSettings { debugLogging: boolean; showConnectionStatus: boolean; autoDetectPortConflicts: boolean; - enableConcurrentSessions: boolean; - maxConcurrentConnections: number; apiKey: string; dangerouslyDisableAuth: boolean; readOnlyMode: boolean; @@ -50,7 +36,6 @@ interface MCPServerInfo { toolsCount: number; resourcesCount: number; connections: number; - concurrentSessions: boolean; poolStats: { enabled: boolean; stats?: { @@ -77,8 +62,6 @@ const DEFAULT_SETTINGS: MCPPluginSettings = { debugLogging: false, showConnectionStatus: true, autoDetectPortConflicts: true, - enableConcurrentSessions: false, // Disabled by default for backward compatibility - maxConcurrentConnections: 32, apiKey: '', // Will be generated on first load dangerouslyDisableAuth: false, // Auth enabled by default readOnlyMode: false, // Read-only mode disabled by default @@ -348,18 +331,16 @@ export default class ObsidianMCPPlugin extends Plugin { getMCPServerInfo(): MCPServerInfo { const poolStats = this.mcpServer?.getConnectionPoolStats(); - const resourceCount = this.settings.enableConcurrentSessions ? 2 : 1; // vault-info + session-info - + return { version: getVersion(), running: this.mcpServer?.isServerRunning() || false, port: this.settings.httpPort, vaultName: this.app.vault.getName(), vaultPath: this.getVaultPath(), - toolsCount: 6, // Our 6 semantic tools (including graph) - resourcesCount: resourceCount, + toolsCount: 6, + resourcesCount: 2, // vault-info + session-info connections: this.mcpServer?.getConnectionCount() || 0, - concurrentSessions: this.settings.enableConcurrentSessions, poolStats: poolStats }; } @@ -614,8 +595,8 @@ class MCPSettingTab extends PluginSettingTab { createStatusItem('Resources', info.resourcesCount.toString()); createStatusItem('Connections', info.connections.toString()); - // Show pool stats if concurrent sessions are enabled - if (info.concurrentSessions && info.poolStats?.enabled && info.poolStats.stats) { + // Show pool stats + if (info.poolStats?.enabled && info.poolStats.stats) { const poolStats = info.poolStats.stats; createStatusItem('Active Sessions', `${poolStats.activeConnections}/${poolStats.maxConnections}`); createStatusItem('Pool Utilization', `${Math.round(poolStats.utilization * 100)}%`, @@ -1245,35 +1226,6 @@ class MCPSettingTab extends PluginSettingTab { await this.plugin.saveSettings(); })); - new Setting(containerEl).setName("Concurrent sessions").setHeading(); - - new Setting(containerEl) - .setName('Enable concurrent sessions for agent swarms') - .setDesc('Allow multiple mcp clients to connect simultaneously. Required for agent swarms and multi-client setups.') - .addToggle(toggle => toggle - .setValue(this.plugin.settings.enableConcurrentSessions) - .onChange(async (value) => { - this.plugin.settings.enableConcurrentSessions = value; - await this.plugin.saveSettings(); - - // Show notice about restart requirement - new Notice('Server restart required for concurrent session changes to take effect'); - })); - - new Setting(containerEl) - .setName('Maximum concurrent connections') - .setDesc('Maximum number of simultaneous connections allowed (1-100, default: 32)') - .addText(text => text - .setPlaceholder('32') - .setValue(this.plugin.settings.maxConcurrentConnections.toString()) - .onChange(async (value) => { - const num = parseInt(value); - if (!isNaN(num) && num >= 1 && num <= 100) { - this.plugin.settings.maxConcurrentConnections = num; - await this.plugin.saveSettings(); - } - })) - .setDisabled(!this.plugin.settings.enableConcurrentSessions); } private createProtocolInfoSection(containerEl: HTMLElement): void { @@ -1331,9 +1283,7 @@ class MCPSettingTab extends PluginSettingTab { new Setting(info).setName("").setHeading(); const resourcesList = info.createEl('ul'); resourcesList.createEl('li', {text: '📊 Obsidian://vault-info - real-time vault metadata'}); - if (this.plugin.settings.enableConcurrentSessions) { - resourcesList.createEl('li', {text: '🔄 Obsidian://session-info - active mcp sessions and statistics'}); - } + resourcesList.createEl('li', {text: '🔄 Obsidian://session-info - active mcp sessions and statistics'}); new Setting(info).setName("Claude code connection").setHeading(); const commandExample = info.createDiv('protocol-command-example'); @@ -1358,16 +1308,15 @@ class MCPSettingTab extends PluginSettingTab { info.createEl('p', { text: 'Add this to your mcp client configuration file:' }); - - // Option 1: Direct HTTP Transport - info.createEl('p', {text: 'Option 1: direct HTTP transport (if supported by your client):', cls: 'mcp-section-header'}); + const configExample = info.createDiv('desktop-config-example'); const configEl = configExample.createEl('pre'); configEl.classList.add('mcp-config-example'); - + + const vaultName = this.app.vault.getName(); const configJson = this.plugin.settings.dangerouslyDisableAuth ? { "mcpServers": { - [this.app.vault.getName()]: { + [vaultName]: { "transport": { "type": "http", "url": `${baseUrl}/mcp` @@ -1376,10 +1325,13 @@ class MCPSettingTab extends PluginSettingTab { } } : { "mcpServers": { - [this.app.vault.getName()]: { + [vaultName]: { "transport": { "type": "http", - "url": `${protocol}://obsidian:${this.plugin.settings.apiKey}@localhost:${port}/mcp` + "url": `${baseUrl}/mcp`, + "headers": { + "Authorization": `Bearer ${this.plugin.settings.apiKey}` + } } } } @@ -1388,99 +1340,7 @@ class MCPSettingTab extends PluginSettingTab { const configJsonText = JSON.stringify(configJson, null, 2); configEl.textContent = configJsonText; - // Add copy button this.addCopyButton(configExample, configJsonText); - - // Option 2: Via mcp-remote - info.createEl('p', {text: 'Option 2: via mcp-remote (for claude desktop):', cls: 'mcp-section-header'}); - info.createEl('p', { - text: 'Mcp-remote supports authentication headers via the --header flag:', - cls: 'setting-item-description mcp-security-note' - }); - - const remoteExample = info.createDiv('desktop-config-example'); - const remoteEl = remoteExample.createEl('pre'); - remoteEl.classList.add('mcp-config-example'); - - // Check if we're using self-signed certificates (HTTPS enabled and auto-generate is on) - const isUsingSelfSignedCert = this.plugin.settings.httpsEnabled && - (this.plugin.settings.certificateConfig.autoGenerate !== false || - !this.plugin.settings.certificateConfig.certPath); - - const vaultName = this.app.vault.getName(); - const remoteEntry: MCPServerEntry = { - command: "npx", - args: this.plugin.settings.dangerouslyDisableAuth - ? ["mcp-remote", `${baseUrl}/mcp`] - : ["mcp-remote", `${baseUrl}/mcp`, "--header", `Authorization: Bearer ${this.plugin.settings.apiKey}`] - }; - if (isUsingSelfSignedCert) { - remoteEntry.env = { "NODE_TLS_REJECT_UNAUTHORIZED": "0" }; - } - const remoteJson: MCPClientConfig = { - mcpServers: { [vaultName]: remoteEntry } - }; - - const remoteJsonText = JSON.stringify(remoteJson, null, 2); - remoteEl.textContent = remoteJsonText; - - // Add copy button - this.addCopyButton(remoteExample, remoteJsonText); - - // Add note about self-signed certificates if applicable - if (isUsingSelfSignedCert) { - info.createEl('p', { - text: '📝 self-signed certificate detected: node_TLS_reject_unauthorized=0 is included to allow the secure connection.', - cls: 'setting-item-description mcp-cert-note' - }); - } - - // Option 2a: Windows Configuration - info.createEl('p', {text: 'Option 2a: Windows configuration (via mcp-remote):', cls: 'mcp-section-header'}); - info.createEl('p', { - text: 'Windows has issues with spaces in npx arguments. Use environment variables to work around this:', - cls: 'setting-item-description mcp-security-note' - }); - - const windowsExample = info.createDiv('desktop-config-example'); - const windowsEl = windowsExample.createEl('pre'); - windowsEl.classList.add('mcp-config-example'); - - const windowsEntry: MCPServerEntry = { - command: "npx", - args: this.plugin.settings.dangerouslyDisableAuth - ? ["mcp-remote", `${baseUrl}/mcp`] - : ["mcp-remote", `${baseUrl}/mcp`, "--header", "Authorization:${OBSIDIAN_API_KEY}"] - }; - const windowsEnv: Record = {}; - if (!this.plugin.settings.dangerouslyDisableAuth) { - windowsEnv["OBSIDIAN_API_KEY"] = `Bearer ${this.plugin.settings.apiKey}`; - } - if (isUsingSelfSignedCert) { - windowsEnv["NODE_TLS_REJECT_UNAUTHORIZED"] = "0"; - } - if (Object.keys(windowsEnv).length > 0) { - windowsEntry.env = windowsEnv; - } - const windowsJson: MCPClientConfig = { - mcpServers: { [vaultName]: windowsEntry } - }; - - const windowsJsonText = JSON.stringify(windowsJson, null, 2); - windowsEl.textContent = windowsJsonText; - - // Add copy button - this.addCopyButton(windowsExample, windowsJsonText); - - const configPath = info.createEl('p', { - text: 'Configuration file location:' - }); - configPath.classList.add('mcp-config-path'); - - const pathList = configPath.createEl('ul'); - pathList.createEl('li', {text: 'macOS: ~/Library/Application Support/Claude/claude_desktop_config.json'}); - pathList.createEl('li', {text: 'Windows: %APPDATA%\\Claude\\claude_desktop_config.json'}); - pathList.createEl('li', {text: 'Linux: ~/.config/Claude/claude_desktop_config.json'}); } private addCopyButton(container: HTMLElement, textToCopy: string): void { diff --git a/src/mcp-server.ts b/src/mcp-server.ts index c229d0f..86e3c9f 100644 --- a/src/mcp-server.ts +++ b/src/mcp-server.ts @@ -1,25 +1,19 @@ import express from 'express'; import cors from 'cors'; -import { App, FileSystemAdapter, Notice } from 'obsidian'; +import { App, Notice } from 'obsidian'; import { createServer as createHttpServer, Server, IncomingMessage, ServerResponse } from 'http'; import { Server as HttpsServer } from 'https'; import { Server as MCPServer } from '@modelcontextprotocol/sdk/server/index.js'; import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js'; import { - ListToolsRequestSchema, - CallToolRequestSchema, - ListResourcesRequestSchema, - ReadResourceRequestSchema, - isInitializeRequest, - type CallToolResult + isInitializeRequest } from '@modelcontextprotocol/sdk/types.js'; import { randomUUID } from 'crypto'; import * as path from 'path'; import { getVersion } from './version'; import { ObsidianAPI } from './utils/obsidian-api'; import { SecureObsidianAPI, VaultSecurityManager } from './security'; -import { semanticTools, createSemanticTools } from './tools/semantic-tools'; -import { DataviewTool, isDataviewToolAvailable } from './tools/dataview-tool'; +import { semanticTools } from './tools/semantic-tools'; import { Debug } from './utils/debug'; import { ConnectionPool, PooledRequest } from './utils/connection-pool'; import { SessionManager } from './utils/session-manager'; @@ -36,8 +30,6 @@ interface MCPPluginRef { httpPort?: number; certificateConfig?: CertificateConfig; readOnlyMode?: boolean; - enableConcurrentSessions?: boolean; - maxConcurrentConnections?: number; apiKey?: string; dangerouslyDisableAuth?: boolean; // From SecurePluginRef (for SecureObsidianAPI) @@ -67,11 +59,6 @@ interface ServerWithTimeouts { setTimeout: (msecs: number) => unknown; } -/** Vault adapter with basePath for file system access */ -interface VaultAdapterWithBasePath extends FileSystemAdapter { - basePath?: string; -} - /** Null response shim for internal initialize calls */ interface NullResponse { writeHead: (status: number, headers?: Record) => void; @@ -109,8 +96,7 @@ interface ConnectionPoolStatsResponse { export class MCPHttpServer { private app: express.Application; private server?: Server | HttpsServer; - private mcpServer?: MCPServer; // Single server for non-concurrent mode - private mcpServerPool?: MCPServerPool; // Server pool for concurrent mode + private mcpServerPool!: MCPServerPool; private transports: Map = new Map(); private obsidianApp: App; private obsidianAPI: ObsidianAPI; @@ -169,248 +155,85 @@ export class MCPHttpServer { // Always use SecureObsidianAPI for consistent security layer this.obsidianAPI = new SecureObsidianAPI(obsidianApp, undefined, plugin, securitySettings); - // Initialize connection pool and session manager if concurrent sessions are enabled - if (plugin?.settings?.enableConcurrentSessions) { - const maxConnections: number = plugin.settings.maxConcurrentConnections ?? 32; - - // Initialize session manager - this.sessionManager = new SessionManager({ - maxSessions: maxConnections, - sessionTimeout: 3600000, // 1 hour - checkInterval: 60000 // Check every minute - }); - this.sessionManager.start(); - - // Handle session events - this.sessionManager.on('session-evicted', (data: { session: { sessionId: string }; reason: string }) => { - // Clean up transport for evicted session - const transport = this.transports.get(data.session.sessionId); - if (transport) { - void transport.close(); - this.transports.delete(data.session.sessionId); - this.connectionCount = Math.max(0, this.connectionCount - 1); - Debug.log(`🔚 Evicted session ${data.session.sessionId} (${data.reason}). Connections: ${this.connectionCount}`); - } - }); - - // Initialize connection pool - this.connectionPool = new ConnectionPool({ - maxConnections, - maxQueueSize: 100, - requestTimeout: 30000, - sessionTimeout: 3600000, - sessionCheckInterval: 60000, - workerScript: path.join(plugin.manifest.dir ?? '', 'dist', 'workers', 'semantic-worker.js') - }); - void this.connectionPool.initialize(); + // Initialize connection pool and session manager (always concurrent) + const maxConnections = 32; - // Set up connection pool request processing - this.connectionPool.on('process', (request: PooledRequest) => { - void (async () => { - try { - // Touch session to update activity - if (request.sessionId && this.sessionManager) { - this.sessionManager.touchSession(request.sessionId); - } + this.sessionManager = new SessionManager({ + maxSessions: maxConnections, + sessionTimeout: 3600000, // 1 hour + checkInterval: 60000 // Check every minute + }); + this.sessionManager.start(); - // Extract tool name from method - const toolName = request.method.replace('tool.', ''); - const tool = semanticTools.find(t => t.name === toolName); + // Handle session events + this.sessionManager.on('session-evicted', (data: { session: { sessionId: string }; reason: string }) => { + const transport = this.transports.get(data.session.sessionId); + if (transport) { + void transport.close(); + this.transports.delete(data.session.sessionId); + this.connectionCount = Math.max(0, this.connectionCount - 1); + Debug.log(`🔚 Evicted session ${data.session.sessionId} (${data.reason}). Connections: ${this.connectionCount}`); + } + }); - if (!tool) { - this.connectionPool!.completeRequest(request.id, { - id: request.id, - error: new Error(`Tool not found: ${toolName}`) - }); - return; - } + // Initialize connection pool + this.connectionPool = new ConnectionPool({ + maxConnections, + maxQueueSize: 100, + requestTimeout: 30000, + sessionTimeout: 3600000, + sessionCheckInterval: 60000, + workerScript: path.join(plugin?.manifest.dir ?? '', 'dist', 'workers', 'semantic-worker.js') + }); + void this.connectionPool.initialize(); - // Create session-specific API instance if needed - const sessionAPI = this.getSessionAPI(request.sessionId); + // Set up connection pool request processing + this.connectionPool.on('process', (request: PooledRequest) => { + void (async () => { + try { + if (request.sessionId && this.sessionManager) { + this.sessionManager.touchSession(request.sessionId); + } - // Check if this operation needs data preparation for worker threads - this.prepareWorkerContext(request); - - // Execute tool with session context - const result = await tool.handler(sessionAPI, request.params); + const toolName = request.method.replace('tool.', ''); + const tool = semanticTools.find(t => t.name === toolName); + if (!tool) { this.connectionPool!.completeRequest(request.id, { id: request.id, - result - }); - } catch (error) { - this.connectionPool!.completeRequest(request.id, { - id: request.id, - error + error: new Error(`Tool not found: ${toolName}`) }); + return; } - })(); - }); - - // Initialize MCP Server Pool for concurrent sessions - this.mcpServerPool = new MCPServerPool(this.obsidianAPI, maxConnections, plugin); - - // Set contexts for session-info resource - this.mcpServerPool.setContexts(this.sessionManager, this.connectionPool); - - Debug.log(`🏊 Connection pool initialized with max ${maxConnections} connections`); - } else { - // Initialize single MCP Server for non-concurrent mode - this.mcpServer = new MCPServer( - { - name: 'Semantic Notes Vault MCP', - version: getVersion() - }, - { - capabilities: { - tools: {}, - resources: {} - } + + const sessionAPI = this.getSessionAPI(request.sessionId); + this.prepareWorkerContext(request); + const result = await tool.handler(sessionAPI, request.params); + + this.connectionPool!.completeRequest(request.id, { + id: request.id, + result + }); + } catch (error) { + this.connectionPool!.completeRequest(request.id, { + id: request.id, + error + }); } - ); - this.setupMCPHandlers(); - } + })(); + }); + + // Initialize MCP Server Pool + this.mcpServerPool = new MCPServerPool(this.obsidianAPI, maxConnections, plugin); + this.mcpServerPool.setContexts(this.sessionManager, this.connectionPool); + + Debug.log(`🏊 Connection pool initialized with max ${maxConnections} connections`); this.app = express(); this.setupMiddleware(); this.setupRoutes(); } - private setupMCPHandlers(): void { - // Only set up handlers for non-concurrent mode - // In concurrent mode, each server in the pool has its own handlers - if (!this.mcpServer) return; - - // Get available tools - const availableTools = createSemanticTools(this.obsidianAPI); - - // List tools handler - this.mcpServer.setRequestHandler(ListToolsRequestSchema, () => { - Debug.log('📋 Listing available tools'); - return { - tools: availableTools.map(tool => ({ - name: tool.name, - description: tool.description, - inputSchema: tool.inputSchema - })) - }; - }); - - // Call tool handler - this.mcpServer.setRequestHandler(CallToolRequestSchema, async (request): Promise => { - const { name, arguments: args } = request.params; - Debug.log(`🔧 Executing tool: ${name}`, args); - - const tool = availableTools.find(t => t.name === name); - if (!tool) { - return { - content: [{ - type: 'text', - text: `Error: Unknown tool "${name}"` - }], - isError: true - }; - } - - try { - const result = await tool.handler(this.obsidianAPI, args || {}); - return result as CallToolResult; - } catch (error) { - Debug.error(`Tool execution error (${name}):`, error); - return { - content: [{ - type: 'text', - text: `Error executing tool "${name}": ${error instanceof Error ? error.message : String(error)}` - }], - isError: true - }; - } - }); - - // Build resources list - const resources = [ - { - uri: 'obsidian://vault-info', - name: 'Vault Information', - description: 'Current vault status, file counts, and metadata', - mimeType: 'application/json' - } - ]; - - // Add Dataview reference if available - if (isDataviewToolAvailable(this.obsidianAPI)) { - resources.push({ - uri: 'obsidian://dataview-reference', - name: 'Dataview Query Language Reference', - description: 'Complete DQL syntax guide with examples, functions, and best practices', - mimeType: 'text/markdown' - }); - } - - // List resources handler - this.mcpServer.setRequestHandler(ListResourcesRequestSchema, () => { - Debug.log('📋 Listing available resources'); - return { resources }; - }); - - // Read resource handler - this.mcpServer.setRequestHandler(ReadResourceRequestSchema, (request) => { - const { uri } = request.params; - Debug.log(`📖 Reading resource: ${uri}`); - - if (uri === 'obsidian://vault-info') { - const vaultName = this.obsidianApp.vault.getName(); - const activeFile = this.obsidianApp.workspace.getActiveFile(); - const allFiles = this.obsidianApp.vault.getAllLoadedFiles(); - const markdownFiles = this.obsidianApp.vault.getMarkdownFiles(); - - const vaultInfo = { - vault: { - name: vaultName, - path: (this.obsidianApp.vault.adapter as unknown as VaultAdapterWithBasePath).basePath ?? 'Unknown' - }, - activeFile: activeFile ? { - name: activeFile.name, - path: activeFile.path, - basename: activeFile.basename, - extension: activeFile.extension - } : null, - files: { - total: allFiles.length, - markdown: markdownFiles.length, - attachments: allFiles.length - markdownFiles.length - }, - plugin: { - version: getVersion(), - status: 'Connected and operational', - transport: 'HTTP MCP via Express.js + MCP SDK' - }, - timestamp: new Date().toISOString() - }; - - return { - contents: [{ - uri: 'obsidian://vault-info', - mimeType: 'application/json', - text: JSON.stringify(vaultInfo, null, 2) - }] - }; - } - - if (uri === 'obsidian://dataview-reference' && isDataviewToolAvailable(this.obsidianAPI)) { - return { - contents: [{ - uri: 'obsidian://dataview-reference', - mimeType: 'text/markdown', - text: DataviewTool.generateDataviewReference() - }] - }; - } - - throw new Error(`Unknown resource: ${uri}`); - }); - } - private setupMiddleware(): void { // CORS middleware for Claude Code and MCP clients this.app.use(cors({ @@ -632,9 +455,8 @@ export class MCPHttpServer { } }; - // Determine which server to use - if (this.mcpServerPool) { - // Concurrent mode - use server pool + // Determine which server to use from the pool + { if (sessionId && this.transports.has(sessionId)) { // Use existing transport for this session transport = this.transports.get(sessionId)!; @@ -713,56 +535,6 @@ export class MCPHttpServer { this.connectionCount++; requireInitializeNotice = true; } - } else { - // Non-concurrent mode - use single MCP server - mcpServer = this.mcpServer!; - if (sessionId && this.transports.has(sessionId)) { - // Use existing transport - transport = this.transports.get(sessionId)!; - effectiveSessionId = sessionId; - if (this.sessionManager) this.sessionManager.touchSession(sessionId); - } else if (sessionId) { - // No active transport for provided session - if (isInitializeRequest(request)) { - effectiveSessionId = sessionId; - transport = new StreamableHTTPServerTransport({ sessionIdGenerator: () => effectiveSessionId }); - await mcpServer.connect(transport); - this.transports.set(effectiveSessionId, transport); - attachTransportHandlers(effectiveSessionId, transport); - this.connectionCount++; - } else { - effectiveSessionId = sessionId; - transport = new StreamableHTTPServerTransport({ sessionIdGenerator: () => effectiveSessionId }); - await mcpServer.connect(transport); - this.transports.set(effectiveSessionId, transport); - attachTransportHandlers(effectiveSessionId, transport); - this.connectionCount++; - requireInitializeNotice = true; - } - } else if (!sessionId && isInitializeRequest(request)) { - // New initialization request - create new session and transport - effectiveSessionId = randomUUID(); - transport = new StreamableHTTPServerTransport({ sessionIdGenerator: () => effectiveSessionId }); - await mcpServer.connect(transport); - this.transports.set(effectiveSessionId, transport); - attachTransportHandlers(effectiveSessionId, transport); - this.connectionCount++; - if (this.sessionManager) { - this.sessionManager.getOrCreateSession(effectiveSessionId); - } - } else { - // No session header and not an initialize request: pre-provision session and require initialize - effectiveSessionId = randomUUID(); - transport = new StreamableHTTPServerTransport({ sessionIdGenerator: () => effectiveSessionId }); - await mcpServer.connect(transport); - this.transports.set(effectiveSessionId, transport); - attachTransportHandlers(effectiveSessionId, transport); - this.connectionCount++; - if (this.sessionManager) { - this.sessionManager.getOrCreateSession(effectiveSessionId); - } - requireInitializeNotice = true; - } } // Compatibility: if we just created a transport for a non-initialize call, diff --git a/src/utils/mcp-server-pool.ts b/src/utils/mcp-server-pool.ts index 13ceb96..e61da9e 100644 --- a/src/utils/mcp-server-pool.ts +++ b/src/utils/mcp-server-pool.ts @@ -21,8 +21,6 @@ import type { ConnectionPool } from './connection-pool'; * can be passed through the constructor chain. */ interface PluginWithSettings { settings?: { - enableConcurrentSessions?: boolean; - maxConcurrentConnections?: number; readOnlyMode?: boolean; // From SecurePluginRef (for SecureObsidianAPI) security?: Partial; @@ -195,8 +193,8 @@ export class MCPServerPool extends EventEmitter { } ]; - // Add session-info resource if concurrent sessions enabled - if (this.plugin?.settings?.enableConcurrentSessions && this.sessionManager) { + // Add session-info resource + if (this.sessionManager) { resources.push({ uri: 'obsidian://session-info', name: 'Session Information', @@ -267,7 +265,7 @@ export class MCPServerPool extends EventEmitter { }; } - if (uri === 'obsidian://session-info' && this.plugin?.settings?.enableConcurrentSessions && this.sessionManager) { + if (uri === 'obsidian://session-info' && this.sessionManager) { const sessions = this.sessionManager.getAllSessions(); const sessionStats = this.sessionManager.getStats(); const poolStats = this.connectionPool?.getStats(); @@ -329,7 +327,7 @@ export class MCPServerPool extends EventEmitter { sessions: sessionData, settings: { sessionTimeout: '1 hour', - maxConcurrentConnections: this.plugin?.settings?.maxConcurrentConnections ?? 32 + maxConcurrentConnections: this.maxServers }, timestamp: new Date().toISOString() }; diff --git a/tests/read-only-mode.test.ts b/tests/read-only-mode.test.ts index 0fe2c86..7cf4181 100644 --- a/tests/read-only-mode.test.ts +++ b/tests/read-only-mode.test.ts @@ -80,8 +80,6 @@ describe('Read-Only Mode Integration', () => { debugLogging: false, showConnectionStatus: true, autoDetectPortConflicts: true, - enableConcurrentSessions: false, - maxConcurrentConnections: 32, apiKey: 'test-key', dangerouslyDisableAuth: false, readOnlyMode: true // This should be present