Files
obsidian-mcp-server/src/settings.ts
Bill 5bc3aeed69 fix: prevent notification settings section from collapsing on toggle
- Add targeted DOM update method for notification section
- Store reference to details element during initial render
- Replace full page re-render with targeted subsection update
- Preserve open/closed state during updates

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-26 09:27:01 -04:00

317 lines
11 KiB
TypeScript

import { App, Notice, PluginSettingTab, Setting } from 'obsidian';
import { MCPPluginSettings } from './types/settings-types';
import MCPServerPlugin from './main';
import { generateApiKey } from './utils/auth-utils';
export class MCPServerSettingTab extends PluginSettingTab {
plugin: MCPServerPlugin;
private notificationDetailsEl: HTMLDetailsElement | null = null;
constructor(app: App, plugin: MCPServerPlugin) {
super(app, plugin);
this.plugin = plugin;
}
/**
* Render notification settings (Show parameters, Notification duration, Log to console, View history)
*/
private renderNotificationSettings(parent: HTMLElement): void {
// Show parameters
new Setting(parent)
.setName('Show parameters')
.setDesc('Include tool parameters in notifications')
.addToggle(toggle => toggle
.setValue(this.plugin.settings.showParameters)
.onChange(async (value) => {
this.plugin.settings.showParameters = value;
await this.plugin.saveSettings();
this.plugin.updateNotificationManager();
}));
// Notification duration
new Setting(parent)
.setName('Notification duration')
.setDesc('Duration in milliseconds')
.addText(text => text
.setPlaceholder('3000')
.setValue(String(this.plugin.settings.notificationDuration))
.onChange(async (value) => {
const duration = parseInt(value);
if (!isNaN(duration) && duration > 0) {
this.plugin.settings.notificationDuration = duration;
await this.plugin.saveSettings();
this.plugin.updateNotificationManager();
}
}));
// Log to console
new Setting(parent)
.setName('Log to console')
.setDesc('Log tool calls to console')
.addToggle(toggle => toggle
.setValue(this.plugin.settings.logToConsole)
.onChange(async (value) => {
this.plugin.settings.logToConsole = value;
await this.plugin.saveSettings();
this.plugin.updateNotificationManager();
}));
// View history button
new Setting(parent)
.setName('Notification history')
.setDesc('View recent MCP tool calls')
.addButton(button => button
.setButtonText('View History')
.onClick(() => {
this.plugin.showNotificationHistory();
}));
}
display(): void {
const {containerEl} = this;
containerEl.empty();
// Clear notification details reference for fresh render
this.notificationDetailsEl = null;
containerEl.createEl('h2', {text: 'MCP Server Settings'});
// Server status
containerEl.createEl('h3', {text: 'Server Status'});
const statusEl = containerEl.createEl('div', {cls: 'mcp-server-status'});
const isRunning = this.plugin.mcpServer?.isRunning() ?? false;
statusEl.createEl('p', {
text: isRunning
? `✅ Running on http://127.0.0.1:${this.plugin.settings.port}/mcp`
: '⭕ Stopped'
});
// Control buttons
const buttonContainer = containerEl.createEl('div', {cls: 'mcp-button-container'});
if (isRunning) {
buttonContainer.createEl('button', {text: 'Stop Server'})
.addEventListener('click', async () => {
await this.plugin.stopServer();
this.display();
});
buttonContainer.createEl('button', {text: 'Restart Server'})
.addEventListener('click', async () => {
await this.plugin.stopServer();
await this.plugin.startServer();
this.display();
});
} else {
buttonContainer.createEl('button', {text: 'Start Server'})
.addEventListener('click', async () => {
await this.plugin.startServer();
this.display();
});
}
// Auto-start setting
new Setting(containerEl)
.setName('Auto-start server')
.setDesc('Start server when Obsidian launches')
.addToggle(toggle => toggle
.setValue(this.plugin.settings.autoStart)
.onChange(async (value) => {
this.plugin.settings.autoStart = value;
await this.plugin.saveSettings();
}));
// Port setting
new Setting(containerEl)
.setName('Port')
.setDesc('Server port (restart required)')
.addText(text => text
.setPlaceholder('3000')
.setValue(String(this.plugin.settings.port))
.onChange(async (value) => {
const port = parseInt(value);
if (!isNaN(port) && port > 0 && port < 65536) {
this.plugin.settings.port = port;
await this.plugin.saveSettings();
if (this.plugin.mcpServer?.isRunning()) {
new Notice('⚠️ Server restart required for port changes to take effect');
}
}
}));
// Authentication (Always Enabled)
const authDetails = containerEl.createEl('details');
authDetails.style.marginBottom = '20px';
const authSummary = authDetails.createEl('summary');
authSummary.style.fontSize = '1.17em';
authSummary.style.fontWeight = 'bold';
authSummary.style.marginBottom = '12px';
authSummary.style.cursor = 'pointer';
authSummary.setText('Authentication');
// API Key Display (always show - auth is always enabled)
new Setting(authDetails)
.setName('API Key Management')
.setDesc('Use as Bearer token in Authorization header');
// Create a full-width container for buttons and key display
const apiKeyContainer = authDetails.createDiv({cls: 'mcp-api-key-section'});
apiKeyContainer.style.marginBottom = '20px';
apiKeyContainer.style.marginLeft = '0';
// Create button container
const apiKeyButtonContainer = apiKeyContainer.createDiv({cls: 'mcp-api-key-buttons'});
apiKeyButtonContainer.style.display = 'flex';
apiKeyButtonContainer.style.gap = '8px';
apiKeyButtonContainer.style.marginBottom = '12px';
// Copy button
const copyButton = apiKeyButtonContainer.createEl('button', {text: '📋 Copy Key'});
copyButton.addEventListener('click', async () => {
await navigator.clipboard.writeText(this.plugin.settings.apiKey || '');
new Notice('✅ API key copied to clipboard');
});
// Regenerate button
const regenButton = apiKeyButtonContainer.createEl('button', {text: '🔄 Regenerate Key'});
regenButton.addEventListener('click', async () => {
this.plugin.settings.apiKey = generateApiKey();
await this.plugin.saveSettings();
new Notice('✅ New API key generated');
if (this.plugin.mcpServer?.isRunning()) {
new Notice('⚠️ Server restart required for API key changes to take effect');
}
this.display();
});
// API Key display (static, copyable text)
const keyDisplayContainer = apiKeyContainer.createDiv({cls: 'mcp-api-key-display'});
keyDisplayContainer.style.padding = '12px';
keyDisplayContainer.style.backgroundColor = 'var(--background-secondary)';
keyDisplayContainer.style.borderRadius = '4px';
keyDisplayContainer.style.fontFamily = 'monospace';
keyDisplayContainer.style.fontSize = '0.9em';
keyDisplayContainer.style.wordBreak = 'break-all';
keyDisplayContainer.style.userSelect = 'all';
keyDisplayContainer.style.cursor = 'text';
keyDisplayContainer.style.marginBottom = '16px';
keyDisplayContainer.textContent = this.plugin.settings.apiKey || '';
// MCP Client Configuration (show always, regardless of auth)
const configDetails = authDetails.createEl('details');
configDetails.style.marginTop = '16px';
const configSummary = configDetails.createEl('summary');
configSummary.style.fontSize = '1em';
configSummary.style.fontWeight = 'bold';
configSummary.style.marginBottom = '8px';
configSummary.style.cursor = 'pointer';
configSummary.setText('MCP Client Configuration');
const configContainer = configDetails.createDiv({cls: 'mcp-config-snippet'});
configContainer.style.marginBottom = '20px';
const configDesc = configContainer.createEl('p', {
text: 'Add to your MCP client config:'
});
configDesc.style.marginBottom = '8px';
configDesc.style.fontSize = '0.9em';
configDesc.style.color = 'var(--text-muted)';
// Generate JSON config (auth always included)
const mcpConfig = {
"mcpServers": {
"obsidian-mcp": {
"serverUrl": `http://127.0.0.1:${this.plugin.settings.port}/mcp`,
"headers": {
"Authorization": `Bearer ${this.plugin.settings.apiKey || 'YOUR_API_KEY_HERE'}`
}
}
}
};
// Config display with copy button
const configButtonContainer = configContainer.createDiv();
configButtonContainer.style.display = 'flex';
configButtonContainer.style.gap = '8px';
configButtonContainer.style.marginBottom = '8px';
const copyConfigButton = configButtonContainer.createEl('button', {text: '📋 Copy Configuration'});
copyConfigButton.addEventListener('click', async () => {
await navigator.clipboard.writeText(JSON.stringify(mcpConfig, null, 2));
new Notice('✅ Configuration copied to clipboard');
});
const configDisplay = configContainer.createEl('pre');
configDisplay.style.padding = '12px';
configDisplay.style.backgroundColor = 'var(--background-secondary)';
configDisplay.style.borderRadius = '4px';
configDisplay.style.fontSize = '0.85em';
configDisplay.style.overflowX = 'auto';
configDisplay.style.userSelect = 'text';
configDisplay.style.cursor = 'text';
configDisplay.textContent = JSON.stringify(mcpConfig, null, 2);
// Notification Settings
const notifDetails = containerEl.createEl('details');
notifDetails.style.marginBottom = '20px';
const notifSummary = notifDetails.createEl('summary');
notifSummary.style.fontSize = '1.17em';
notifSummary.style.fontWeight = 'bold';
notifSummary.style.marginBottom = '12px';
notifSummary.style.cursor = 'pointer';
notifSummary.setText('UI Notifications');
// Store reference for targeted updates
this.notificationDetailsEl = notifDetails;
// Enable notifications
new Setting(notifDetails)
.setName('Enable notifications')
.setDesc('Show when MCP tools are called')
.addToggle(toggle => toggle
.setValue(this.plugin.settings.notificationsEnabled)
.onChange(async (value) => {
this.plugin.settings.notificationsEnabled = value;
await this.plugin.saveSettings();
this.plugin.updateNotificationManager();
this.updateNotificationSection();
}));
// Show notification settings only if enabled
if (this.plugin.settings.notificationsEnabled) {
this.renderNotificationSettings(notifDetails);
}
}
/**
* Update only the notification section without re-rendering entire page
*/
private updateNotificationSection(): void {
if (!this.notificationDetailsEl) {
// Fallback to full re-render if reference lost
this.display();
return;
}
// Store current open state
const wasOpen = this.notificationDetailsEl.open;
// Find and remove all child elements except the summary
const summary = this.notificationDetailsEl.querySelector('summary');
while (this.notificationDetailsEl.lastChild && this.notificationDetailsEl.lastChild !== summary) {
this.notificationDetailsEl.removeChild(this.notificationDetailsEl.lastChild);
}
// Rebuild notification settings
if (this.plugin.settings.notificationsEnabled) {
this.renderNotificationSettings(this.notificationDetailsEl);
}
// Restore open state
this.notificationDetailsEl.open = wasOpen;
}
}