From 27b5c45f271fc60819cf52e1f36e891da765d291 Mon Sep 17 00:00:00 2001 From: Bryan Van Deusen Date: Tue, 26 May 2026 21:23:06 -0400 Subject: [PATCH] =?UTF-8?q?feat(ui):=20MCP=20Access=20tab=20=E2=80=94=20HT?= =?UTF-8?q?TP=20transport,=20in-app=20endpoint?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Rewrites the apikeys settings tab for the new MCP architecture: - Tab label: 'API Keys' → 'MCP Access' - Shows the in-app MCP URL (/mcp) with a copy button - Claude Code snippet uses --transport http + --url + --header - Claude Desktop snippet uses {url, headers: {Authorization}} - Drops the wheel-download flow, the 'Other' client tab, and the stdio env file / Claude config download helpers — those were for the standalone fable-mcp package which goes away in phase 8 The api_keys backend stays unchanged — keys double as bearer tokens for the /mcp endpoint via the existing auth.py middleware. Co-Authored-By: Claude Opus 4.7 (1M context) --- frontend/src/views/SettingsView.vue | 311 ++++++++++------------------ 1 file changed, 115 insertions(+), 196 deletions(-) diff --git a/frontend/src/views/SettingsView.vue b/frontend/src/views/SettingsView.vue index 3a32c18..4f009e8 100644 --- a/frontend/src/views/SettingsView.vue +++ b/frontend/src/views/SettingsView.vue @@ -3,7 +3,7 @@ import { ref, computed, watch, onMounted } from "vue"; import { useSettingsStore } from "@/stores/settings"; import { useAuthStore } from "@/stores/auth"; import { useToastStore } from "@/stores/toast"; -import { apiGet, apiPost, apiPut, apiDelete, listGroups, createGroup, deleteGroup, listGroupMembers, addGroupMember, removeGroupMember, searchUsers, getFableMcpInfo, listApiKeys, createApiKey as apiCreateApiKey, revokeApiKey as apiRevokeApiKey, getVoiceStatus, getVoiceList, getVoiceLibrary, installVoice, uninstallVoice, synthesiseSpeech, getProfile, updateProfile, consolidateProfile, clearProfileObservations, listProfileObservations, getJournalConfig, saveJournalConfig, geocodeAddress, type ApiKeyEntry, type GroupEntry, type GroupMember, type UserSearchResult, type VoiceStatusResult, type VoiceEntry, type VoiceLibraryEntry, type UserProfile, type JournalConfig, type ProfileObservationEntry } from "@/api/client"; +import { apiGet, apiPost, apiPut, apiDelete, listGroups, createGroup, deleteGroup, listGroupMembers, addGroupMember, removeGroupMember, searchUsers, listApiKeys, createApiKey as apiCreateApiKey, revokeApiKey as apiRevokeApiKey, getVoiceStatus, getVoiceList, getVoiceLibrary, installVoice, uninstallVoice, synthesiseSpeech, getProfile, updateProfile, consolidateProfile, clearProfileObservations, listProfileObservations, getJournalConfig, saveJournalConfig, geocodeAddress, type ApiKeyEntry, type GroupEntry, type GroupMember, type UserSearchResult, type VoiceStatusResult, type VoiceEntry, type VoiceLibraryEntry, type UserProfile, type JournalConfig, type ProfileObservationEntry } from "@/api/client"; import { usePushStore } from "@/stores/push"; import type { User } from "@/types/auth"; import PaginationBar from "@/components/PaginationBar.vue"; @@ -93,7 +93,7 @@ function _loadTabContent(tab: string) { else if (tab === "groups") loadGroupsPanel(); } if (tab === "voice") loadVoiceTab(); - if (tab === "apikeys") { fetchApiKeys(); loadMcpInfo(); } + if (tab === "apikeys") { fetchApiKeys(); } } watch(activeTab, (v) => { @@ -101,7 +101,7 @@ watch(activeTab, (v) => { _loadTabContent(v); }); -// API Keys +// MCP Access (API Keys are used as Bearer tokens for the in-app /mcp endpoint) const apiKeys = ref([]); const newKeyName = ref(''); const newKeyScope = ref<'read' | 'write'>('write'); @@ -109,34 +109,38 @@ const newKeyValue = ref(''); const apiKeyCopied = ref(false); const creatingApiKey = ref(false); const revokeConfirmId = ref(null); -const mcpInfo = ref<{ available: boolean; filename: string | null } | null>(null); -const mcpInfoLoading = ref(false); const origin = window.location.origin; -const mcpClientTab = ref<'claude-code' | 'claude-desktop' | 'other'>('claude-code'); +const mcpUrl = computed(() => `${origin}/mcp`); +const mcpUrlCopied = ref(false); +const mcpClientTab = ref<'claude-code' | 'claude-desktop'>('claude-code'); const copiedSnippetKey = ref(null); -const effectiveApiKey = computed(() => newKeyValue.value || ''); +const effectiveApiKey = computed(() => newKeyValue.value || ''); const claudeCodeCommand = computed(() => { - return `claude mcp add --transport stdio --scope user fable \\ - --env FABLE_URL=${origin} \\ - --env FABLE_API_KEY=${effectiveApiKey.value} \\ - -- fable-mcp`; + return `claude mcp add --transport http --scope user fable \\ + --url ${mcpUrl.value} \\ + --header "Authorization: Bearer ${effectiveApiKey.value}"`; }); const mcpConfigSnippet = computed(() => JSON.stringify({ mcpServers: { fable: { - command: "fable-mcp", - env: { - FABLE_URL: origin, - FABLE_API_KEY: effectiveApiKey.value, + url: mcpUrl.value, + headers: { + Authorization: `Bearer ${effectiveApiKey.value}`, }, }, }, }, null, 2)); +async function copyMcpUrl() { + await copyToClipboard(mcpUrl.value); + mcpUrlCopied.value = true; + setTimeout(() => { mcpUrlCopied.value = false; }, 2000); +} + async function copyToClipboard(text: string) { try { await navigator.clipboard.writeText(text); @@ -162,18 +166,6 @@ async function copySnippet(text: string, key: string) { }, 2000); } -async function loadMcpInfo() { - if (mcpInfo.value !== null) return; - mcpInfoLoading.value = true; - try { - mcpInfo.value = await getFableMcpInfo(); - } catch { - mcpInfo.value = { available: false, filename: null }; - } finally { - mcpInfoLoading.value = false; - } -} - async function fetchApiKeys() { apiKeys.value = await listApiKeys(); } @@ -203,38 +195,6 @@ async function copyApiKey() { setTimeout(() => { apiKeyCopied.value = false; }, 2000); } -function _downloadFile(filename: string, content: string, mimeType = 'text/plain') { - const blob = new Blob([content], { type: mimeType }); - const url = URL.createObjectURL(blob); - const a = document.createElement('a'); - a.href = url; - a.download = filename; - a.click(); - URL.revokeObjectURL(url); -} - -function downloadEnvFile() { - const baseUrl = window.location.origin; - const content = `FABLE_URL=${baseUrl}\nFABLE_API_KEY=${newKeyValue.value}\n`; - _downloadFile('fable-mcp.env', content); -} - -function downloadMcpConfig() { - const baseUrl = window.location.origin; - const config = { - mcpServers: { - fable: { - command: 'fable-mcp', - env: { - FABLE_URL: baseUrl, - FABLE_API_KEY: newKeyValue.value, - }, - }, - }, - }; - _downloadFile('fable-mcp-config.json', JSON.stringify(config, null, 2), 'application/json'); -} - // Groups management const groups = ref([]); const groupsLoading = ref(false); @@ -1501,7 +1461,7 @@ function formatUserDate(iso: string): string { :class="['sidebar-item', { active: activeTab === tab }]" @click="activeTab = tab" > - {{ tab === 'apikeys' ? 'API Keys' : tab.charAt(0).toUpperCase() + tab.slice(1) }} + {{ tab === 'apikeys' ? 'MCP Access' : tab.charAt(0).toUpperCase() + tab.slice(1) }} - +
+
-

API Keys

+

MCP Access

- API keys let external tools (like the Fable MCP server) access your data without a browser session. - Write-scoped keys can create and update content; read-only keys can only query. + Connect Claude (Code or Desktop) to this Fable instance. Claude reads and writes your notes, + tasks, projects, events, and people via the built-in MCP endpoint below. +

+ +
+ +
+
{{ mcpUrl }}
+ +
+
+
+ + +
+

Personal access tokens

+

+ Tokens are sent as Authorization: Bearer … on every MCP request and resolve to + your user account. Write-scoped tokens can create and update content; read-only tokens can + only query. A token is shown once at creation — keep it safe.

- +
- +
-

Copy this key now — it will not be shown again.

+

Copy this token now — it will not be shown again.

{{ newKeyValue }}
+

+ The snippets in Connect Claude below are pre-filled with this token until you click Done. +

- -
- + @@ -2573,140 +2555,70 @@ function formatUserDate(iso: string): string {
NameScopePrefixLast Used
-

No API keys yet.

+

No tokens yet.

- +
-

Fable MCP

+

Connect Claude

- The Fable MCP server lets Claude (and other MCP clients) read and write your notes, tasks, and projects. - Install it locally, then point it at this Fable instance with an API key above. + Run the appropriate snippet for your client. Generate a token above first to pre-fill the + token value, or copy the snippet now and paste your own where it says <your-token>.

-
Checking package availability…
- + +
  • + Restart Claude Desktop. The Fable tools should appear in the available tools list. +
  • +
    @@ -4302,6 +4214,13 @@ FABLE_API_KEY={{ effectiveApiKey }} border-bottom-color: var(--color-primary); font-weight: 500; } +.mcp-url-block { margin-top: 0.25rem; } +.mcp-url-label { + display: block; + font-size: 0.85rem; + opacity: 0.7; + margin-bottom: 0.25rem; +} .mcp-code-row { display: flex; align-items: stretch;