403 lines
14 KiB
TypeScript
403 lines
14 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;
|
|
private activeConfigTab: 'windsurf' | 'claude-code' = 'windsurf';
|
|
|
|
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();
|
|
}));
|
|
}
|
|
|
|
/**
|
|
* Generate client-specific MCP configuration
|
|
*/
|
|
private generateConfigForClient(client: 'windsurf' | 'claude-code'): {
|
|
filePath: string;
|
|
config: object;
|
|
usageNote: string;
|
|
} {
|
|
const port = this.plugin.settings.port;
|
|
const apiKey = this.plugin.settings.apiKey || 'YOUR_API_KEY_HERE';
|
|
|
|
if (client === 'windsurf') {
|
|
return {
|
|
filePath: '~/.windsurf/config.json',
|
|
config: {
|
|
"mcpServers": {
|
|
"obsidian": {
|
|
"serverUrl": `http://127.0.0.1:${port}/mcp`,
|
|
"headers": {
|
|
"Authorization": `Bearer ${apiKey}`
|
|
}
|
|
}
|
|
}
|
|
},
|
|
usageNote: 'After copying, paste into the config file and restart Windsurf.'
|
|
};
|
|
} else { // claude-code
|
|
return {
|
|
filePath: '~/.claude.json',
|
|
config: {
|
|
"mcpServers": {
|
|
"obsidian": {
|
|
"type": "http",
|
|
"url": `http://127.0.0.1:${port}/mcp`,
|
|
"headers": {
|
|
"Authorization": `Bearer ${apiKey}`
|
|
}
|
|
}
|
|
}
|
|
},
|
|
usageNote: 'After copying, paste into the config file and restart Claude Code.'
|
|
};
|
|
}
|
|
}
|
|
|
|
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 & Configuration');
|
|
|
|
// 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 heading
|
|
const configHeading = authDetails.createEl('h4', {text: 'MCP Client Configuration'});
|
|
configHeading.style.marginTop = '24px';
|
|
configHeading.style.marginBottom = '12px';
|
|
|
|
const configContainer = authDetails.createDiv({cls: 'mcp-config-snippet'});
|
|
configContainer.style.marginBottom = '20px';
|
|
|
|
// Tab buttons for switching between clients
|
|
const tabContainer = configContainer.createDiv({cls: 'mcp-config-tabs'});
|
|
tabContainer.style.display = 'flex';
|
|
tabContainer.style.gap = '8px';
|
|
tabContainer.style.marginBottom = '16px';
|
|
tabContainer.style.borderBottom = '1px solid var(--background-modifier-border)';
|
|
|
|
// Windsurf tab button
|
|
const windsurfTab = tabContainer.createEl('button', {text: 'Windsurf'});
|
|
windsurfTab.style.padding = '8px 16px';
|
|
windsurfTab.style.border = 'none';
|
|
windsurfTab.style.background = 'none';
|
|
windsurfTab.style.cursor = 'pointer';
|
|
windsurfTab.style.borderBottom = this.activeConfigTab === 'windsurf'
|
|
? '2px solid var(--interactive-accent)'
|
|
: '2px solid transparent';
|
|
windsurfTab.style.fontWeight = this.activeConfigTab === 'windsurf' ? 'bold' : 'normal';
|
|
windsurfTab.addEventListener('click', () => {
|
|
this.activeConfigTab = 'windsurf';
|
|
this.display();
|
|
});
|
|
|
|
// Claude Code tab button
|
|
const claudeCodeTab = tabContainer.createEl('button', {text: 'Claude Code'});
|
|
claudeCodeTab.style.padding = '8px 16px';
|
|
claudeCodeTab.style.border = 'none';
|
|
claudeCodeTab.style.background = 'none';
|
|
claudeCodeTab.style.cursor = 'pointer';
|
|
claudeCodeTab.style.borderBottom = this.activeConfigTab === 'claude-code'
|
|
? '2px solid var(--interactive-accent)'
|
|
: '2px solid transparent';
|
|
claudeCodeTab.style.fontWeight = this.activeConfigTab === 'claude-code' ? 'bold' : 'normal';
|
|
claudeCodeTab.addEventListener('click', () => {
|
|
this.activeConfigTab = 'claude-code';
|
|
this.display();
|
|
});
|
|
|
|
// Get configuration for active tab
|
|
const {filePath, config, usageNote} = this.generateConfigForClient(this.activeConfigTab);
|
|
|
|
// Tab content area
|
|
const tabContent = configContainer.createDiv({cls: 'mcp-config-content'});
|
|
tabContent.style.marginTop = '16px';
|
|
|
|
// File location label
|
|
const fileLocationLabel = tabContent.createEl('p', {text: 'Configuration file location:'});
|
|
fileLocationLabel.style.marginBottom = '4px';
|
|
fileLocationLabel.style.fontSize = '0.9em';
|
|
fileLocationLabel.style.color = 'var(--text-muted)';
|
|
|
|
// File path display
|
|
const filePathDisplay = tabContent.createEl('div', {text: filePath});
|
|
filePathDisplay.style.padding = '8px';
|
|
filePathDisplay.style.backgroundColor = 'var(--background-secondary)';
|
|
filePathDisplay.style.borderRadius = '4px';
|
|
filePathDisplay.style.fontFamily = 'monospace';
|
|
filePathDisplay.style.fontSize = '0.9em';
|
|
filePathDisplay.style.marginBottom = '12px';
|
|
filePathDisplay.style.color = 'var(--text-muted)';
|
|
|
|
// Copy button
|
|
const copyConfigButton = tabContent.createEl('button', {text: '📋 Copy Configuration'});
|
|
copyConfigButton.style.marginBottom = '12px';
|
|
copyConfigButton.addEventListener('click', async () => {
|
|
await navigator.clipboard.writeText(JSON.stringify(config, null, 2));
|
|
new Notice('✅ Configuration copied to clipboard');
|
|
});
|
|
|
|
// Config JSON display
|
|
const configDisplay = tabContent.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.style.marginBottom = '12px';
|
|
configDisplay.textContent = JSON.stringify(config, null, 2);
|
|
|
|
// Usage note
|
|
const usageNoteDisplay = tabContent.createEl('p', {text: usageNote});
|
|
usageNoteDisplay.style.fontSize = '0.9em';
|
|
usageNoteDisplay.style.color = 'var(--text-muted)';
|
|
usageNoteDisplay.style.fontStyle = 'italic';
|
|
|
|
// 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;
|
|
}
|
|
}
|