Skip to main content
Glama

Chrome MCP Server

App.vue49.1 kB
<template> <div class="popup-container"> <div class="header"> <div class="header-content"> <h1 class="header-title">Chrome MCP Server</h1> </div> </div> <div class="content"> <div class="section"> <h2 class="section-title">{{ getMessage('nativeServerConfigLabel') }}</h2> <div class="config-card"> <div class="status-section"> <div class="status-header"> <p class="status-label">{{ getMessage('runningStatusLabel') }}</p> <button class="refresh-status-button" @click="refreshServerStatus" :title="getMessage('refreshStatusButton')" > 🔄 </button> </div> <div class="status-info"> <span :class="['status-dot', getStatusClass()]"></span> <span class="status-text">{{ getStatusText() }}</span> </div> <div v-if="serverStatus.lastUpdated" class="status-timestamp"> {{ getMessage('lastUpdatedLabel') }} {{ new Date(serverStatus.lastUpdated).toLocaleTimeString() }} </div> </div> <div v-if="showMcpConfig" class="mcp-config-section"> <div class="mcp-config-header"> <p class="mcp-config-label">{{ getMessage('mcpServerConfigLabel') }}</p> <button class="copy-config-button" @click="copyMcpConfig"> {{ copyButtonText }} </button> </div> <div class="mcp-config-content"> <pre class="mcp-config-json">{{ mcpConfigJson }}</pre> </div> </div> <div class="port-section"> <label for="port" class="port-label">{{ getMessage('connectionPortLabel') }}</label> <input type="text" id="port" :value="nativeServerPort" @input="updatePort" class="port-input" /> </div> <button class="connect-button" :disabled="isConnecting" @click="testNativeConnection"> <BoltIcon /> <span>{{ isConnecting ? getMessage('connectingStatus') : nativeConnectionStatus === 'connected' ? getMessage('disconnectButton') : getMessage('connectButton') }}</span> </button> </div> </div> <div class="section"> <h2 class="section-title">{{ getMessage('semanticEngineLabel') }}</h2> <div class="semantic-engine-card"> <div class="semantic-engine-status"> <div class="status-info"> <span :class="['status-dot', getSemanticEngineStatusClass()]"></span> <span class="status-text">{{ getSemanticEngineStatusText() }}</span> </div> <div v-if="semanticEngineLastUpdated" class="status-timestamp"> {{ getMessage('lastUpdatedLabel') }} {{ new Date(semanticEngineLastUpdated).toLocaleTimeString() }} </div> </div> <ProgressIndicator v-if="isSemanticEngineInitializing" :visible="isSemanticEngineInitializing" :text="semanticEngineInitProgress" :showSpinner="true" /> <button class="semantic-engine-button" :disabled="isSemanticEngineInitializing" @click="initializeSemanticEngine" > <BoltIcon /> <span>{{ getSemanticEngineButtonText() }}</span> </button> </div> </div> <div class="section"> <h2 class="section-title">{{ getMessage('embeddingModelLabel') }}</h2> <ProgressIndicator v-if="isModelSwitching || isModelDownloading" :visible="isModelSwitching || isModelDownloading" :text="getProgressText()" :showSpinner="true" /> <div v-if="modelInitializationStatus === 'error'" class="error-card"> <div class="error-content"> <div class="error-icon">⚠️</div> <div class="error-details"> <p class="error-title">{{ getMessage('semanticEngineInitFailedStatus') }}</p> <p class="error-message">{{ modelErrorMessage || getMessage('semanticEngineInitFailedStatus') }}</p> <p class="error-suggestion">{{ getErrorTypeText() }}</p> </div> </div> <button class="retry-button" @click="retryModelInitialization" :disabled="isModelSwitching || isModelDownloading" > <span>🔄</span> <span>{{ getMessage('retryButton') }}</span> </button> </div> <div class="model-list"> <div v-for="model in availableModels" :key="model.preset" :class="[ 'model-card', { selected: currentModel === model.preset, disabled: isModelSwitching || isModelDownloading, }, ]" @click=" !isModelSwitching && !isModelDownloading && switchModel(model.preset as ModelPreset) " > <div class="model-header"> <div class="model-info"> <p class="model-name" :class="{ 'selected-text': currentModel === model.preset }"> {{ model.preset }} </p> <p class="model-description">{{ getModelDescription(model) }}</p> </div> <div v-if="currentModel === model.preset" class="check-icon"> <CheckIcon class="text-white" /> </div> </div> <div class="model-tags"> <span class="model-tag performance">{{ getPerformanceText(model.performance) }}</span> <span class="model-tag size">{{ model.size }}</span> <span class="model-tag dimension">{{ model.dimension }}D</span> </div> </div> </div> </div> <div class="section"> <h2 class="section-title">{{ getMessage('indexDataManagementLabel') }}</h2> <div class="stats-grid"> <div class="stats-card"> <div class="stats-header"> <p class="stats-label">{{ getMessage('indexedPagesLabel') }}</p> <span class="stats-icon violet"> <DocumentIcon /> </span> </div> <p class="stats-value">{{ storageStats?.indexedPages || 0 }}</p> </div> <div class="stats-card"> <div class="stats-header"> <p class="stats-label">{{ getMessage('indexSizeLabel') }}</p> <span class="stats-icon teal"> <DatabaseIcon /> </span> </div> <p class="stats-value">{{ formatIndexSize() }}</p> </div> <div class="stats-card"> <div class="stats-header"> <p class="stats-label">{{ getMessage('activeTabsLabel') }}</p> <span class="stats-icon blue"> <TabIcon /> </span> </div> <p class="stats-value">{{ getActiveTabsCount() }}</p> </div> <div class="stats-card"> <div class="stats-header"> <p class="stats-label">{{ getMessage('vectorDocumentsLabel') }}</p> <span class="stats-icon green"> <VectorIcon /> </span> </div> <p class="stats-value">{{ storageStats?.totalDocuments || 0 }}</p> </div> </div> <ProgressIndicator v-if="isClearingData && clearDataProgress" :visible="isClearingData" :text="clearDataProgress" :showSpinner="true" /> <button class="danger-button" :disabled="isClearingData" @click="showClearConfirmation = true" > <TrashIcon /> <span>{{ isClearingData ? getMessage('clearingStatus') : getMessage('clearAllDataButton') }}</span> </button> </div> <!-- Model Cache Management Section --> <ModelCacheManagement :cache-stats="cacheStats" :is-managing-cache="isManagingCache" @cleanup-cache="cleanupCache" @clear-all-cache="clearAllCache" /> </div> <div class="footer"> <p class="footer-text">chrome mcp server for ai</p> </div> <ConfirmDialog :visible="showClearConfirmation" :title="getMessage('confirmClearDataTitle')" :message="getMessage('clearDataWarningMessage')" :items="[ getMessage('clearDataList1'), getMessage('clearDataList2'), getMessage('clearDataList3'), ]" :warning="getMessage('clearDataIrreversibleWarning')" icon="⚠️" :confirm-text="getMessage('confirmClearButton')" :cancel-text="getMessage('cancelButton')" :confirming-text="getMessage('clearingStatus')" :is-confirming="isClearingData" @confirm="confirmClearAllData" @cancel="hideClearDataConfirmation" /> </div> </template> <script lang="ts" setup> import { ref, onMounted, onUnmounted, computed } from 'vue'; import { PREDEFINED_MODELS, type ModelPreset, getModelInfo, getCacheStats, clearModelCache, cleanupModelCache, } from '@/utils/semantic-similarity-engine'; import { BACKGROUND_MESSAGE_TYPES } from '@/common/message-types'; import { getMessage } from '@/utils/i18n'; import ConfirmDialog from './components/ConfirmDialog.vue'; import ProgressIndicator from './components/ProgressIndicator.vue'; import ModelCacheManagement from './components/ModelCacheManagement.vue'; import { DocumentIcon, DatabaseIcon, BoltIcon, TrashIcon, CheckIcon, TabIcon, VectorIcon, } from './components/icons'; const nativeConnectionStatus = ref<'unknown' | 'connected' | 'disconnected'>('unknown'); const isConnecting = ref(false); const nativeServerPort = ref<number>(12306); const serverStatus = ref<{ isRunning: boolean; port?: number; lastUpdated: number; }>({ isRunning: false, lastUpdated: Date.now(), }); const showMcpConfig = computed(() => { return nativeConnectionStatus.value === 'connected' && serverStatus.value.isRunning; }); const copyButtonText = ref(getMessage('copyConfigButton')); const mcpConfigJson = computed(() => { const port = serverStatus.value.port || nativeServerPort.value; const config = { mcpServers: { 'streamable-mcp-server': { type: 'streamable-http', url: `http://127.0.0.1:${port}/mcp`, }, }, }; return JSON.stringify(config, null, 2); }); const currentModel = ref<ModelPreset | null>(null); const isModelSwitching = ref(false); const modelSwitchProgress = ref(''); const modelDownloadProgress = ref<number>(0); const isModelDownloading = ref(false); const modelInitializationStatus = ref<'idle' | 'downloading' | 'initializing' | 'ready' | 'error'>( 'idle', ); const modelErrorMessage = ref<string>(''); const modelErrorType = ref<'network' | 'file' | 'unknown' | ''>(''); const selectedVersion = ref<'quantized'>('quantized'); const storageStats = ref<{ indexedPages: number; totalDocuments: number; totalTabs: number; indexSize: number; isInitialized: boolean; } | null>(null); const isRefreshingStats = ref(false); const isClearingData = ref(false); const showClearConfirmation = ref(false); const clearDataProgress = ref(''); const semanticEngineStatus = ref<'idle' | 'initializing' | 'ready' | 'error'>('idle'); const isSemanticEngineInitializing = ref(false); const semanticEngineInitProgress = ref(''); const semanticEngineLastUpdated = ref<number | null>(null); // Cache management const isManagingCache = ref(false); const cacheStats = ref<{ totalSize: number; totalSizeMB: number; entryCount: number; entries: Array<{ url: string; size: number; sizeMB: number; timestamp: number; age: string; expired: boolean; }>; } | null>(null); const availableModels = computed(() => { return Object.entries(PREDEFINED_MODELS).map(([key, value]) => ({ preset: key as ModelPreset, ...value, })); }); const getStatusClass = () => { if (nativeConnectionStatus.value === 'connected') { if (serverStatus.value.isRunning) { return 'bg-emerald-500'; } else { return 'bg-yellow-500'; } } else if (nativeConnectionStatus.value === 'disconnected') { return 'bg-red-500'; } else { return 'bg-gray-500'; } }; const getStatusText = () => { if (nativeConnectionStatus.value === 'connected') { if (serverStatus.value.isRunning) { return getMessage('serviceRunningStatus', [(serverStatus.value.port || 'Unknown').toString()]); } else { return getMessage('connectedServiceNotStartedStatus'); } } else if (nativeConnectionStatus.value === 'disconnected') { return getMessage('serviceNotConnectedStatus'); } else { return getMessage('detectingStatus'); } }; const formatIndexSize = () => { if (!storageStats.value?.indexSize) return '0 MB'; const sizeInMB = Math.round(storageStats.value.indexSize / (1024 * 1024)); return `${sizeInMB} MB`; }; const getModelDescription = (model: any) => { switch (model.preset) { case 'multilingual-e5-small': return getMessage('lightweightModelDescription'); case 'multilingual-e5-base': return getMessage('betterThanSmallDescription'); default: return getMessage('multilingualModelDescription'); } }; const getPerformanceText = (performance: string) => { switch (performance) { case 'fast': return getMessage('fastPerformance'); case 'balanced': return getMessage('balancedPerformance'); case 'accurate': return getMessage('accuratePerformance'); default: return performance; } }; const getSemanticEngineStatusText = () => { switch (semanticEngineStatus.value) { case 'ready': return getMessage('semanticEngineReadyStatus'); case 'initializing': return getMessage('semanticEngineInitializingStatus'); case 'error': return getMessage('semanticEngineInitFailedStatus'); case 'idle': default: return getMessage('semanticEngineNotInitStatus'); } }; const getSemanticEngineStatusClass = () => { switch (semanticEngineStatus.value) { case 'ready': return 'bg-emerald-500'; case 'initializing': return 'bg-yellow-500'; case 'error': return 'bg-red-500'; case 'idle': default: return 'bg-gray-500'; } }; const getActiveTabsCount = () => { return storageStats.value?.totalTabs || 0; }; const getProgressText = () => { if (isModelDownloading.value) { return getMessage('downloadingModelStatus', [modelDownloadProgress.value.toString()]); } else if (isModelSwitching.value) { return modelSwitchProgress.value || getMessage('switchingModelStatus'); } return ''; }; const getErrorTypeText = () => { switch (modelErrorType.value) { case 'network': return getMessage('networkErrorMessage'); case 'file': return getMessage('modelCorruptedErrorMessage'); case 'unknown': default: return getMessage('unknownErrorMessage'); } }; const getSemanticEngineButtonText = () => { switch (semanticEngineStatus.value) { case 'ready': return getMessage('reinitializeButton'); case 'initializing': return getMessage('initializingStatus'); case 'error': return getMessage('reinitializeButton'); case 'idle': default: return getMessage('initSemanticEngineButton'); } }; const loadCacheStats = async () => { try { cacheStats.value = await getCacheStats(); } catch (error) { console.error('Failed to get cache stats:', error); cacheStats.value = null; } }; const cleanupCache = async () => { if (isManagingCache.value) return; isManagingCache.value = true; try { await cleanupModelCache(); // Refresh cache stats await loadCacheStats(); } catch (error) { console.error('Failed to cleanup cache:', error); } finally { isManagingCache.value = false; } }; const clearAllCache = async () => { if (isManagingCache.value) return; isManagingCache.value = true; try { await clearModelCache(); // Refresh cache stats await loadCacheStats(); } catch (error) { console.error('Failed to clear cache:', error); } finally { isManagingCache.value = false; } }; const saveSemanticEngineState = async () => { try { const semanticEngineState = { status: semanticEngineStatus.value, lastUpdated: semanticEngineLastUpdated.value, }; // eslint-disable-next-line no-undef await chrome.storage.local.set({ semanticEngineState }); } catch (error) { console.error('保存语义引擎状态失败:', error); } }; const initializeSemanticEngine = async () => { if (isSemanticEngineInitializing.value) return; const isReinitialization = semanticEngineStatus.value === 'ready'; console.log( `🚀 User triggered semantic engine ${isReinitialization ? 'reinitialization' : 'initialization'}`, ); isSemanticEngineInitializing.value = true; semanticEngineStatus.value = 'initializing'; semanticEngineInitProgress.value = isReinitialization ? getMessage('semanticEngineInitializingStatus') : getMessage('semanticEngineInitializingStatus'); semanticEngineLastUpdated.value = Date.now(); await saveSemanticEngineState(); try { // eslint-disable-next-line no-undef chrome.runtime .sendMessage({ type: BACKGROUND_MESSAGE_TYPES.INITIALIZE_SEMANTIC_ENGINE, }) .catch((error) => { console.error('❌ Error sending semantic engine initialization request:', error); }); startSemanticEngineStatusPolling(); semanticEngineInitProgress.value = isReinitialization ? getMessage('processingStatus') : getMessage('processingStatus'); } catch (error: any) { console.error('❌ Failed to send initialization request:', error); semanticEngineStatus.value = 'error'; semanticEngineInitProgress.value = `Failed to send initialization request: ${error?.message || 'Unknown error'}`; await saveSemanticEngineState(); setTimeout(() => { semanticEngineInitProgress.value = ''; }, 5000); isSemanticEngineInitializing.value = false; semanticEngineLastUpdated.value = Date.now(); await saveSemanticEngineState(); } }; const checkSemanticEngineStatus = async () => { try { // eslint-disable-next-line no-undef const response = await chrome.runtime.sendMessage({ type: BACKGROUND_MESSAGE_TYPES.GET_MODEL_STATUS, }); if (response && response.success && response.status) { const status = response.status; if (status.initializationStatus === 'ready') { semanticEngineStatus.value = 'ready'; semanticEngineLastUpdated.value = Date.now(); isSemanticEngineInitializing.value = false; semanticEngineInitProgress.value = getMessage('semanticEngineReadyStatus'); await saveSemanticEngineState(); stopSemanticEngineStatusPolling(); setTimeout(() => { semanticEngineInitProgress.value = ''; }, 2000); } else if ( status.initializationStatus === 'downloading' || status.initializationStatus === 'initializing' ) { semanticEngineStatus.value = 'initializing'; isSemanticEngineInitializing.value = true; semanticEngineInitProgress.value = getMessage('semanticEngineInitializingStatus'); semanticEngineLastUpdated.value = Date.now(); await saveSemanticEngineState(); } else if (status.initializationStatus === 'error') { semanticEngineStatus.value = 'error'; semanticEngineLastUpdated.value = Date.now(); isSemanticEngineInitializing.value = false; semanticEngineInitProgress.value = getMessage('semanticEngineInitFailedStatus'); await saveSemanticEngineState(); stopSemanticEngineStatusPolling(); setTimeout(() => { semanticEngineInitProgress.value = ''; }, 5000); } else { semanticEngineStatus.value = 'idle'; isSemanticEngineInitializing.value = false; await saveSemanticEngineState(); } } else { semanticEngineStatus.value = 'idle'; isSemanticEngineInitializing.value = false; await saveSemanticEngineState(); } } catch (error) { console.error('Popup: Failed to check semantic engine status:', error); semanticEngineStatus.value = 'idle'; isSemanticEngineInitializing.value = false; await saveSemanticEngineState(); } }; const retryModelInitialization = async () => { if (!currentModel.value) return; console.log('🔄 Retrying model initialization...'); modelErrorMessage.value = ''; modelErrorType.value = ''; modelInitializationStatus.value = 'downloading'; modelDownloadProgress.value = 0; isModelDownloading.value = true; await switchModel(currentModel.value); }; const updatePort = async (event: Event) => { const target = event.target as HTMLInputElement; const newPort = Number(target.value); nativeServerPort.value = newPort; await savePortPreference(newPort); }; const checkNativeConnection = async () => { try { // eslint-disable-next-line no-undef const response = await chrome.runtime.sendMessage({ type: 'ping_native' }); nativeConnectionStatus.value = response?.connected ? 'connected' : 'disconnected'; } catch (error) { console.error('检测 Native 连接状态失败:', error); nativeConnectionStatus.value = 'disconnected'; } }; const checkServerStatus = async () => { try { // eslint-disable-next-line no-undef const response = await chrome.runtime.sendMessage({ type: BACKGROUND_MESSAGE_TYPES.GET_SERVER_STATUS, }); if (response?.success && response.serverStatus) { serverStatus.value = response.serverStatus; } if (response?.connected !== undefined) { nativeConnectionStatus.value = response.connected ? 'connected' : 'disconnected'; } } catch (error) { console.error('检测服务器状态失败:', error); } }; const refreshServerStatus = async () => { try { // eslint-disable-next-line no-undef const response = await chrome.runtime.sendMessage({ type: BACKGROUND_MESSAGE_TYPES.REFRESH_SERVER_STATUS, }); if (response?.success && response.serverStatus) { serverStatus.value = response.serverStatus; } if (response?.connected !== undefined) { nativeConnectionStatus.value = response.connected ? 'connected' : 'disconnected'; } } catch (error) { console.error('刷新服务器状态失败:', error); } }; const copyMcpConfig = async () => { try { await navigator.clipboard.writeText(mcpConfigJson.value); copyButtonText.value = '✅' + getMessage('configCopiedNotification'); setTimeout(() => { copyButtonText.value = getMessage('copyConfigButton'); }, 2000); } catch (error) { console.error('复制配置失败:', error); copyButtonText.value = '❌' + getMessage('networkErrorMessage'); setTimeout(() => { copyButtonText.value = getMessage('copyConfigButton'); }, 2000); } }; const testNativeConnection = async () => { if (isConnecting.value) return; isConnecting.value = true; try { if (nativeConnectionStatus.value === 'connected') { // eslint-disable-next-line no-undef await chrome.runtime.sendMessage({ type: 'disconnect_native' }); nativeConnectionStatus.value = 'disconnected'; } else { console.log(`尝试连接到端口: ${nativeServerPort.value}`); // eslint-disable-next-line no-undef const response = await chrome.runtime.sendMessage({ type: 'connectNative', port: nativeServerPort.value, }); if (response && response.success) { nativeConnectionStatus.value = 'connected'; console.log('连接成功:', response); await savePortPreference(nativeServerPort.value); } else { nativeConnectionStatus.value = 'disconnected'; console.error('连接失败:', response); } } } catch (error) { console.error('测试连接失败:', error); nativeConnectionStatus.value = 'disconnected'; } finally { isConnecting.value = false; } }; const loadModelPreference = async () => { try { // eslint-disable-next-line no-undef const result = await chrome.storage.local.get([ 'selectedModel', 'selectedVersion', 'modelState', 'semanticEngineState', ]); if (result.selectedModel) { const storedModel = result.selectedModel as string; console.log('📋 Stored model from storage:', storedModel); if (PREDEFINED_MODELS[storedModel as ModelPreset]) { currentModel.value = storedModel as ModelPreset; console.log(`✅ Loaded valid model: ${currentModel.value}`); } else { console.warn( `⚠️ Stored model "${storedModel}" not found in PREDEFINED_MODELS, using default`, ); currentModel.value = 'multilingual-e5-small'; await saveModelPreference(currentModel.value); } } else { console.log('⚠️ No model found in storage, using default'); currentModel.value = 'multilingual-e5-small'; await saveModelPreference(currentModel.value); } selectedVersion.value = 'quantized'; console.log('✅ Using quantized version (fixed)'); await saveVersionPreference('quantized'); if (result.modelState) { const modelState = result.modelState; if (modelState.status === 'ready') { modelInitializationStatus.value = 'ready'; modelDownloadProgress.value = modelState.downloadProgress || 100; isModelDownloading.value = false; } else { modelInitializationStatus.value = 'idle'; modelDownloadProgress.value = 0; isModelDownloading.value = false; await saveModelState(); } } else { modelInitializationStatus.value = 'idle'; modelDownloadProgress.value = 0; isModelDownloading.value = false; } if (result.semanticEngineState) { const semanticState = result.semanticEngineState; if (semanticState.status === 'ready') { semanticEngineStatus.value = 'ready'; semanticEngineLastUpdated.value = semanticState.lastUpdated || Date.now(); } else if (semanticState.status === 'error') { semanticEngineStatus.value = 'error'; semanticEngineLastUpdated.value = semanticState.lastUpdated || Date.now(); } else { semanticEngineStatus.value = 'idle'; } } else { semanticEngineStatus.value = 'idle'; } } catch (error) { console.error('❌ 加载模型偏好失败:', error); } }; const saveModelPreference = async (model: ModelPreset) => { try { // eslint-disable-next-line no-undef await chrome.storage.local.set({ selectedModel: model }); } catch (error) { console.error('保存模型偏好失败:', error); } }; const saveVersionPreference = async (version: 'full' | 'quantized' | 'compressed') => { try { // eslint-disable-next-line no-undef await chrome.storage.local.set({ selectedVersion: version }); } catch (error) { console.error('保存版本偏好失败:', error); } }; const savePortPreference = async (port: number) => { try { // eslint-disable-next-line no-undef await chrome.storage.local.set({ nativeServerPort: port }); console.log(`端口偏好已保存: ${port}`); } catch (error) { console.error('保存端口偏好失败:', error); } }; const loadPortPreference = async () => { try { // eslint-disable-next-line no-undef const result = await chrome.storage.local.get(['nativeServerPort']); if (result.nativeServerPort) { nativeServerPort.value = result.nativeServerPort; console.log(`端口偏好已加载: ${result.nativeServerPort}`); } } catch (error) { console.error('加载端口偏好失败:', error); } }; const saveModelState = async () => { try { const modelState = { status: modelInitializationStatus.value, downloadProgress: modelDownloadProgress.value, isDownloading: isModelDownloading.value, lastUpdated: Date.now(), }; // eslint-disable-next-line no-undef await chrome.storage.local.set({ modelState }); } catch (error) { console.error('保存模型状态失败:', error); } }; let statusMonitoringInterval: ReturnType<typeof setInterval> | null = null; let semanticEngineStatusPollingInterval: ReturnType<typeof setInterval> | null = null; const startModelStatusMonitoring = () => { if (statusMonitoringInterval) { clearInterval(statusMonitoringInterval); } statusMonitoringInterval = setInterval(async () => { try { // eslint-disable-next-line no-undef const response = await chrome.runtime.sendMessage({ type: 'get_model_status', }); if (response && response.success) { const status = response.status; modelInitializationStatus.value = status.initializationStatus || 'idle'; modelDownloadProgress.value = status.downloadProgress || 0; isModelDownloading.value = status.isDownloading || false; if (status.initializationStatus === 'error') { modelErrorMessage.value = status.errorMessage || getMessage('modelFailedStatus'); modelErrorType.value = status.errorType || 'unknown'; } else { modelErrorMessage.value = ''; modelErrorType.value = ''; } await saveModelState(); if (status.initializationStatus === 'ready' || status.initializationStatus === 'error') { stopModelStatusMonitoring(); } } } catch (error) { console.error('获取模型状态失败:', error); } }, 1000); }; const stopModelStatusMonitoring = () => { if (statusMonitoringInterval) { clearInterval(statusMonitoringInterval); statusMonitoringInterval = null; } }; const startSemanticEngineStatusPolling = () => { if (semanticEngineStatusPollingInterval) { clearInterval(semanticEngineStatusPollingInterval); } semanticEngineStatusPollingInterval = setInterval(async () => { try { await checkSemanticEngineStatus(); } catch (error) { console.error('Semantic engine status polling failed:', error); } }, 2000); }; const stopSemanticEngineStatusPolling = () => { if (semanticEngineStatusPollingInterval) { clearInterval(semanticEngineStatusPollingInterval); semanticEngineStatusPollingInterval = null; } }; const refreshStorageStats = async () => { if (isRefreshingStats.value) return; isRefreshingStats.value = true; try { console.log('🔄 Refreshing storage statistics...'); // eslint-disable-next-line no-undef const response = await chrome.runtime.sendMessage({ type: 'get_storage_stats', }); if (response && response.success) { storageStats.value = { indexedPages: response.stats.indexedPages || 0, totalDocuments: response.stats.totalDocuments || 0, totalTabs: response.stats.totalTabs || 0, indexSize: response.stats.indexSize || 0, isInitialized: response.stats.isInitialized || false, }; console.log('✅ Storage stats refreshed:', storageStats.value); } else { console.error('❌ Failed to get storage stats:', response?.error); storageStats.value = { indexedPages: 0, totalDocuments: 0, totalTabs: 0, indexSize: 0, isInitialized: false, }; } } catch (error) { console.error('❌ Error refreshing storage stats:', error); storageStats.value = { indexedPages: 0, totalDocuments: 0, totalTabs: 0, indexSize: 0, isInitialized: false, }; } finally { isRefreshingStats.value = false; } }; const hideClearDataConfirmation = () => { showClearConfirmation.value = false; }; const confirmClearAllData = async () => { if (isClearingData.value) return; isClearingData.value = true; clearDataProgress.value = getMessage('clearingStatus'); try { console.log('🗑️ Starting to clear all data...'); // eslint-disable-next-line no-undef const response = await chrome.runtime.sendMessage({ type: 'clear_all_data', }); if (response && response.success) { clearDataProgress.value = getMessage('dataClearedNotification'); console.log('✅ All data cleared successfully'); await refreshStorageStats(); setTimeout(() => { clearDataProgress.value = ''; hideClearDataConfirmation(); }, 2000); } else { throw new Error(response?.error || 'Failed to clear data'); } } catch (error: any) { console.error('❌ Failed to clear all data:', error); clearDataProgress.value = `Failed to clear data: ${error?.message || 'Unknown error'}`; setTimeout(() => { clearDataProgress.value = ''; }, 5000); } finally { isClearingData.value = false; } }; const switchModel = async (newModel: ModelPreset) => { console.log(`🔄 switchModel called with newModel: ${newModel}`); if (isModelSwitching.value) { console.log('⏸️ Model switch already in progress, skipping'); return; } const isSameModel = newModel === currentModel.value; const currentModelInfo = currentModel.value ? getModelInfo(currentModel.value) : getModelInfo('multilingual-e5-small'); const newModelInfo = getModelInfo(newModel); const isDifferentDimension = currentModelInfo.dimension !== newModelInfo.dimension; console.log(`📊 Switch analysis:`); console.log(` - Same model: ${isSameModel} (${currentModel.value} -> ${newModel})`); console.log( ` - Current dimension: ${currentModelInfo.dimension}, New dimension: ${newModelInfo.dimension}`, ); console.log(` - Different dimension: ${isDifferentDimension}`); if (isSameModel && !isDifferentDimension) { console.log('✅ Same model and dimension - no need to switch'); return; } const switchReasons = []; if (!isSameModel) switchReasons.push('different model'); if (isDifferentDimension) switchReasons.push('different dimension'); console.log(`🚀 Switching model due to: ${switchReasons.join(', ')}`); console.log( `📋 Model: ${currentModel.value} (${currentModelInfo.dimension}D) -> ${newModel} (${newModelInfo.dimension}D)`, ); isModelSwitching.value = true; modelSwitchProgress.value = getMessage('switchingModelStatus'); modelInitializationStatus.value = 'downloading'; modelDownloadProgress.value = 0; isModelDownloading.value = true; try { await saveModelPreference(newModel); await saveVersionPreference('quantized'); await saveModelState(); modelSwitchProgress.value = getMessage('semanticEngineInitializingStatus'); startModelStatusMonitoring(); // eslint-disable-next-line no-undef const response = await chrome.runtime.sendMessage({ type: 'switch_semantic_model', modelPreset: newModel, modelVersion: 'quantized', modelDimension: newModelInfo.dimension, previousDimension: currentModelInfo.dimension, }); if (response && response.success) { currentModel.value = newModel; modelSwitchProgress.value = getMessage('successNotification'); console.log( '模型切换成功:', newModel, 'version: quantized', 'dimension:', newModelInfo.dimension, ); modelInitializationStatus.value = 'ready'; isModelDownloading.value = false; await saveModelState(); setTimeout(() => { modelSwitchProgress.value = ''; }, 2000); } else { throw new Error(response?.error || 'Model switch failed'); } } catch (error: any) { console.error('模型切换失败:', error); modelSwitchProgress.value = `Model switch failed: ${error?.message || 'Unknown error'}`; modelInitializationStatus.value = 'error'; isModelDownloading.value = false; const errorMessage = error?.message || '未知错误'; if ( errorMessage.includes('network') || errorMessage.includes('fetch') || errorMessage.includes('timeout') ) { modelErrorType.value = 'network'; modelErrorMessage.value = getMessage('networkErrorMessage'); } else if ( errorMessage.includes('corrupt') || errorMessage.includes('invalid') || errorMessage.includes('format') ) { modelErrorType.value = 'file'; modelErrorMessage.value = getMessage('modelCorruptedErrorMessage'); } else { modelErrorType.value = 'unknown'; modelErrorMessage.value = errorMessage; } await saveModelState(); setTimeout(() => { modelSwitchProgress.value = ''; }, 8000); } finally { isModelSwitching.value = false; } }; const setupServerStatusListener = () => { // eslint-disable-next-line no-undef chrome.runtime.onMessage.addListener((message) => { if (message.type === BACKGROUND_MESSAGE_TYPES.SERVER_STATUS_CHANGED && message.payload) { serverStatus.value = message.payload; console.log('Server status updated:', message.payload); } }); }; onMounted(async () => { await loadPortPreference(); await loadModelPreference(); await checkNativeConnection(); await checkServerStatus(); await refreshStorageStats(); await loadCacheStats(); await checkSemanticEngineStatus(); setupServerStatusListener(); }); onUnmounted(() => { stopModelStatusMonitoring(); stopSemanticEngineStatusPolling(); }); </script> <style scoped> .popup-container { background: #f1f5f9; border-radius: 24px; box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.25); display: flex; flex-direction: column; overflow: hidden; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; } .header { flex-shrink: 0; padding-left: 20px; } .header-content { display: flex; justify-content: space-between; align-items: center; } .header-title { font-size: 24px; font-weight: 700; color: #1e293b; margin: 0; } .settings-button { padding: 8px; border-radius: 50%; color: #64748b; background: none; border: none; cursor: pointer; transition: all 0.2s ease; } .settings-button:hover { background: #e2e8f0; color: #1e293b; } .content { flex-grow: 1; padding: 8px 24px; overflow-y: auto; scrollbar-width: none; -ms-overflow-style: none; } .content::-webkit-scrollbar { display: none; } .status-card { background: white; border-radius: 16px; box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1); padding: 20px; margin-bottom: 20px; } .status-label { font-size: 14px; font-weight: 500; color: #64748b; margin-bottom: 8px; } .status-info { display: flex; align-items: center; gap: 8px; } .status-dot { height: 8px; width: 8px; border-radius: 50%; } .status-dot.bg-emerald-500 { background-color: #10b981; } .status-dot.bg-red-500 { background-color: #ef4444; } .status-dot.bg-yellow-500 { background-color: #eab308; } .status-dot.bg-gray-500 { background-color: #6b7280; } .status-text { font-size: 16px; font-weight: 600; color: #1e293b; } .model-label { font-size: 14px; font-weight: 500; color: #64748b; margin-bottom: 4px; } .model-name { font-weight: 600; color: #7c3aed; } .stats-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; } .stats-card { background: white; border-radius: 12px; box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1); padding: 16px; } .stats-header { display: flex; align-items: center; justify-content: space-between; margin-bottom: 8px; } .stats-label { font-size: 14px; font-weight: 500; color: #64748b; } .stats-icon { padding: 8px; border-radius: 8px; } .stats-icon.violet { background: #ede9fe; color: #7c3aed; } .stats-icon.teal { background: #ccfbf1; color: #0d9488; } .stats-icon.blue { background: #dbeafe; color: #2563eb; } .stats-icon.green { background: #dcfce7; color: #16a34a; } .stats-value { font-size: 30px; font-weight: 700; color: #0f172a; margin: 0; } .section { margin-bottom: 24px; } .secondary-button { background: #f1f5f9; color: #475569; border: 1px solid #cbd5e1; padding: 8px 16px; border-radius: 8px; font-size: 14px; font-weight: 500; cursor: pointer; transition: all 0.2s ease; display: flex; align-items: center; gap: 8px; } .secondary-button:hover:not(:disabled) { background: #e2e8f0; border-color: #94a3b8; } .secondary-button:disabled { opacity: 0.5; cursor: not-allowed; } .primary-button { background: #3b82f6; color: white; border: none; padding: 8px 16px; border-radius: 8px; font-size: 14px; font-weight: 500; cursor: pointer; transition: all 0.2s ease; } .primary-button:hover { background: #2563eb; } .section-title { font-size: 16px; font-weight: 600; color: #374151; margin-bottom: 12px; } .current-model-card { background: linear-gradient(135deg, #faf5ff, #f3e8ff); border: 1px solid #e9d5ff; border-radius: 12px; padding: 16px; margin-bottom: 16px; } .current-model-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 8px; } .current-model-label { font-size: 14px; font-weight: 500; color: #64748b; margin: 0; } .current-model-badge { background: #8b5cf6; color: white; font-size: 12px; font-weight: 600; padding: 4px 8px; border-radius: 6px; } .current-model-name { font-size: 16px; font-weight: 700; color: #7c3aed; margin: 0; } .model-list { display: flex; flex-direction: column; gap: 12px; } .model-card { background: white; border-radius: 12px; padding: 16px; cursor: pointer; border: 1px solid #e5e7eb; transition: all 0.2s ease; } .model-card:hover { border-color: #8b5cf6; } .model-card.selected { border: 2px solid #8b5cf6; background: #faf5ff; } .model-card.disabled { opacity: 0.5; cursor: not-allowed; pointer-events: none; } .model-header { display: flex; justify-content: space-between; align-items: flex-start; } .model-info { flex: 1; } .model-name { font-weight: 600; color: #1e293b; margin: 0 0 4px 0; } .model-name.selected-text { color: #7c3aed; } .model-description { font-size: 14px; color: #64748b; margin: 0; } .check-icon { width: 20px; height: 20px; flex-shrink: 0; background: #8b5cf6; border-radius: 50%; display: flex; align-items: center; justify-content: center; } .model-tags { display: flex; align-items: center; gap: 8px; margin-top: 16px; } .model-tag { display: inline-flex; align-items: center; border-radius: 9999px; padding: 4px 10px; font-size: 12px; font-weight: 500; } .model-tag.performance { background: #d1fae5; color: #065f46; } .model-tag.size { background: #ddd6fe; color: #5b21b6; } .model-tag.dimension { background: #e5e7eb; color: #4b5563; } .config-card { background: white; border-radius: 16px; box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1); padding: 20px; display: flex; flex-direction: column; gap: 16px; } .semantic-engine-card { background: white; border-radius: 16px; box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1); padding: 20px; display: flex; flex-direction: column; gap: 16px; } .semantic-engine-status { display: flex; flex-direction: column; gap: 8px; } .semantic-engine-button { width: 100%; display: flex; align-items: center; justify-content: center; gap: 8px; background: #8b5cf6; color: white; font-weight: 600; padding: 12px 16px; border-radius: 8px; border: none; cursor: pointer; transition: all 0.2s ease; box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.05); } .semantic-engine-button:hover:not(:disabled) { background: #7c3aed; box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1); } .semantic-engine-button:disabled { opacity: 0.6; cursor: not-allowed; } .status-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 8px; } .refresh-status-button { background: none; border: none; cursor: pointer; padding: 4px 8px; border-radius: 6px; font-size: 14px; color: #64748b; transition: all 0.2s ease; } .refresh-status-button:hover { background: #f1f5f9; color: #374151; } .status-timestamp { font-size: 12px; color: #9ca3af; margin-top: 4px; } .mcp-config-section { border-top: 1px solid #f1f5f9; } .mcp-config-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 8px; } .mcp-config-label { font-size: 14px; font-weight: 500; color: #64748b; margin: 0; } .copy-config-button { background: none; border: none; cursor: pointer; padding: 4px 8px; border-radius: 6px; font-size: 14px; color: #64748b; transition: all 0.2s ease; display: flex; align-items: center; gap: 4px; } .copy-config-button:hover { background: #f1f5f9; color: #374151; } .mcp-config-content { background: #f8fafc; border: 1px solid #e2e8f0; border-radius: 8px; padding: 12px; overflow-x: auto; } .mcp-config-json { font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace; font-size: 12px; line-height: 1.4; color: #374151; margin: 0; white-space: pre; overflow-x: auto; } .port-section { display: flex; flex-direction: column; gap: 8px; } .port-label { font-size: 14px; font-weight: 500; color: #64748b; } .port-input { display: block; width: 100%; border-radius: 8px; border: 1px solid #d1d5db; box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.05); padding: 12px; font-size: 14px; background: #f8fafc; } .port-input:focus { outline: none; border-color: #8b5cf6; box-shadow: 0 0 0 3px rgba(139, 92, 246, 0.1); } .connect-button { width: 100%; display: flex; align-items: center; justify-content: center; gap: 8px; background: #8b5cf6; color: white; font-weight: 600; padding: 12px 16px; border-radius: 8px; border: none; cursor: pointer; transition: all 0.2s ease; box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.05); } .connect-button:hover:not(:disabled) { background: #7c3aed; box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1); } .connect-button:disabled { opacity: 0.6; cursor: not-allowed; } .error-card { background: #fef2f2; border: 1px solid #fecaca; border-radius: 12px; padding: 16px; margin-bottom: 16px; display: flex; align-items: flex-start; gap: 16px; } .error-content { flex: 1; display: flex; align-items: flex-start; gap: 12px; } .error-icon { font-size: 20px; flex-shrink: 0; margin-top: 2px; } .error-details { flex: 1; } .error-title { font-size: 14px; font-weight: 600; color: #dc2626; margin: 0 0 4px 0; } .error-message { font-size: 14px; color: #991b1b; margin: 0 0 8px 0; font-weight: 500; } .error-suggestion { font-size: 13px; color: #7f1d1d; margin: 0; line-height: 1.4; } .retry-button { display: flex; align-items: center; gap: 6px; background: #dc2626; color: white; font-weight: 600; padding: 8px 16px; border-radius: 8px; border: none; cursor: pointer; transition: all 0.2s ease; font-size: 14px; flex-shrink: 0; } .retry-button:hover:not(:disabled) { background: #b91c1c; } .retry-button:disabled { opacity: 0.6; cursor: not-allowed; } .danger-button { width: 100%; display: flex; align-items: center; justify-content: center; gap: 8px; background: white; border: 1px solid #d1d5db; color: #374151; font-weight: 600; padding: 12px 16px; border-radius: 8px; cursor: pointer; transition: all 0.2s ease; margin-top: 16px; } .danger-button:hover:not(:disabled) { border-color: #ef4444; color: #dc2626; } .danger-button:disabled { opacity: 0.6; cursor: not-allowed; } .icon-small { width: 14px; height: 14px; } .icon-default { width: 20px; height: 20px; } .icon-medium { width: 24px; height: 24px; } .footer { padding: 16px; margin-top: auto; } .footer-text { text-align: center; font-size: 12px; color: #94a3b8; margin: 0; } @media (max-width: 320px) { .popup-container { width: 100%; height: 100vh; border-radius: 0; } .header { padding: 24px 20px 12px; } .content { padding: 8px 20px; } .stats-grid { grid-template-columns: 1fr; gap: 8px; } .config-card { padding: 16px; gap: 12px; } .current-model-card { padding: 12px; margin-bottom: 12px; } .stats-card { padding: 12px; } .stats-value { font-size: 24px; } } </style>

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/haithemobeidi/mcp-chrome'

If you have feedback or need assistance with the MCP directory API, please join our Discord server