refactor: Remove concurrent mode toggle, simplify connection setup (ADR-100)

Always run in pooled/concurrent mode — the dual code path (single
MCPServer vs MCPServerPool) existed for backward compatibility with
clients that couldn't handle HTTP transport. All modern MCP clients
now support Streamable HTTP natively.

Changes:
- Remove enableConcurrentSessions toggle and maxConcurrentConnections
  setting from interfaces, defaults, and settings UI
- Remove single-server code path (setupMCPHandlers, non-concurrent
  request handling branch) — ~200 lines of dead code
- Simplify connection templates from 4 options to 2:
  Claude Code command + standard JSON with Authorization header
- Drop mcp-remote and Windows workaround templates
- Fix auth format: use standard Authorization header instead of
  URL-embedded credentials
- Remove unused imports (schema types, DataviewTool, FileSystemAdapter)
- Update README and troubleshooting docs
- Accept ADR-100

Net: -433 lines
This commit is contained in:
Aaron Bockelie 2026-03-14 23:13:35 -05:00
parent 04715c1dfd
commit ae03b2e5f9
8 changed files with 102 additions and 535 deletions

View file

@ -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:

View file

@ -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 |

View file

@ -1,5 +1,5 @@
---
status: Draft
status: Accepted
date: 2026-03-14
deciders:
- aaronsb

View file

@ -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

View file

@ -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<string, string>;
transport?: {
type: string;
url: string;
};
}
interface MCPClientConfig {
mcpServers: Record<string, MCPServerEntry>;
}
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<string, string> = {};
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 {

View file

@ -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<string, string>) => 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<string, StreamableHTTPServerTransport> = 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<CallToolResult> => {
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,

View file

@ -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<import('../security/vault-security-manager').SecuritySettings>;
@ -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()
};

View file

@ -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