feat(ui): MCP Access tab — HTTP transport, in-app endpoint
Rewrites the apikeys settings tab for the new MCP architecture:
- Tab label: 'API Keys' → 'MCP Access'
- Shows the in-app MCP URL (<origin>/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) <noreply@anthropic.com>
This commit is contained in:
+115
-196
@@ -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<ApiKeyEntry[]>([]);
|
||||
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<number | null>(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<string | null>(null);
|
||||
|
||||
const effectiveApiKey = computed(() => newKeyValue.value || '<your-api-key>');
|
||||
const effectiveApiKey = computed(() => newKeyValue.value || '<your-token>');
|
||||
|
||||
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<GroupEntry[]>([]);
|
||||
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) }}
|
||||
</button>
|
||||
</div>
|
||||
<div v-if="authStore.isAdmin" class="sidebar-group">
|
||||
@@ -2514,42 +2474,64 @@ function formatUserDate(iso: string): string {
|
||||
|
||||
</div>
|
||||
|
||||
<!-- ── API Keys ── -->
|
||||
<!-- ── MCP Access ── -->
|
||||
<div v-show="activeTab === 'apikeys'" class="settings-grid">
|
||||
<!-- Endpoint URL -->
|
||||
<section class="settings-section full-width">
|
||||
<h2>API Keys</h2>
|
||||
<h2>MCP Access</h2>
|
||||
<p class="settings-description">
|
||||
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.
|
||||
</p>
|
||||
|
||||
<div class="mcp-url-block">
|
||||
<label class="mcp-url-label">MCP endpoint</label>
|
||||
<div class="mcp-code-row">
|
||||
<pre class="mcp-code">{{ mcpUrl }}</pre>
|
||||
<button class="btn btn-secondary btn-sm" @click="copyMcpUrl">
|
||||
{{ mcpUrlCopied ? 'Copied' : 'Copy URL' }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Tokens -->
|
||||
<section class="settings-section full-width">
|
||||
<h2>Personal access tokens</h2>
|
||||
<p class="settings-description">
|
||||
Tokens are sent as <code>Authorization: Bearer …</code> 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 <strong>once</strong> at creation — keep it safe.
|
||||
</p>
|
||||
|
||||
<!-- Create form -->
|
||||
<div class="api-key-create-form">
|
||||
<input v-model="newKeyName" placeholder="Key name (e.g. Claude MCP)" class="settings-input" />
|
||||
<input v-model="newKeyName" placeholder="Token name (e.g. claude-laptop)" class="settings-input" />
|
||||
<div class="api-key-scope-select">
|
||||
<label><input type="radio" v-model="newKeyScope" value="read" /> Read-only</label>
|
||||
<label><input type="radio" v-model="newKeyScope" value="write" /> Read + Write</label>
|
||||
</div>
|
||||
<button @click="createApiKey" :disabled="!newKeyName || creatingApiKey" class="btn btn-primary">
|
||||
{{ creatingApiKey ? 'Generating…' : 'Generate Key' }}
|
||||
{{ creatingApiKey ? 'Generating…' : 'Generate token' }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- One-time key reveal -->
|
||||
<!-- One-time token reveal -->
|
||||
<div v-if="newKeyValue" class="api-key-reveal">
|
||||
<p><strong>Copy this key now — it will not be shown again.</strong></p>
|
||||
<p><strong>Copy this token now — it will not be shown again.</strong></p>
|
||||
<div class="api-key-value-row">
|
||||
<code class="api-key-value">{{ newKeyValue }}</code>
|
||||
<button @click="copyApiKey" class="btn btn-secondary btn-sm">{{ apiKeyCopied ? 'Copied!' : 'Copy' }}</button>
|
||||
</div>
|
||||
<p class="mcp-hint" style="margin-top:0.5rem;">
|
||||
The snippets in <strong>Connect Claude</strong> below are pre-filled with this token until you click Done.
|
||||
</p>
|
||||
<div style="display:flex; gap:0.5rem; margin-top: 0.5rem;">
|
||||
<button @click="downloadEnvFile" class="btn btn-secondary btn-sm">Download .env</button>
|
||||
<button @click="downloadMcpConfig" class="btn btn-secondary btn-sm">Download Claude config</button>
|
||||
<button @click="newKeyValue = ''" class="btn btn-secondary">Done</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Keys table -->
|
||||
<!-- Tokens table -->
|
||||
<table v-if="apiKeys.length > 0" class="api-keys-table">
|
||||
<thead>
|
||||
<tr><th>Name</th><th>Scope</th><th>Prefix</th><th>Last Used</th><th></th></tr>
|
||||
@@ -2573,140 +2555,70 @@ function formatUserDate(iso: string): string {
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<p v-else-if="apiKeys.length === 0 && activeTab === 'apikeys'" class="settings-empty">No API keys yet.</p>
|
||||
<p v-else-if="apiKeys.length === 0 && activeTab === 'apikeys'" class="settings-empty">No tokens yet.</p>
|
||||
</section>
|
||||
|
||||
<!-- Fable MCP install section -->
|
||||
<!-- Connect Claude -->
|
||||
<section class="settings-section full-width">
|
||||
<h2>Fable MCP</h2>
|
||||
<h2>Connect Claude</h2>
|
||||
<p class="settings-description">
|
||||
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 <code><your-token></code>.
|
||||
</p>
|
||||
|
||||
<div v-if="mcpInfoLoading" class="mcp-status">Checking package availability…</div>
|
||||
<template v-else-if="mcpInfo">
|
||||
<div v-if="mcpInfo.available" class="mcp-available">
|
||||
<div class="mcp-pkg-row">
|
||||
<span class="mcp-pkg-name">{{ mcpInfo.filename }}</span>
|
||||
<a href="/api/fable-mcp/download" class="btn btn-primary btn-sm" download>Download</a>
|
||||
<div class="mcp-client-tabs" role="tablist">
|
||||
<button
|
||||
type="button"
|
||||
role="tab"
|
||||
:aria-selected="mcpClientTab === 'claude-code'"
|
||||
:class="['mcp-client-tab', { active: mcpClientTab === 'claude-code' }]"
|
||||
@click="mcpClientTab = 'claude-code'"
|
||||
>Claude Code</button>
|
||||
<button
|
||||
type="button"
|
||||
role="tab"
|
||||
:aria-selected="mcpClientTab === 'claude-desktop'"
|
||||
:class="['mcp-client-tab', { active: mcpClientTab === 'claude-desktop' }]"
|
||||
@click="mcpClientTab = 'claude-desktop'"
|
||||
>Claude Desktop</button>
|
||||
</div>
|
||||
|
||||
<!-- Claude Code tab -->
|
||||
<ol v-if="mcpClientTab === 'claude-code'">
|
||||
<li>
|
||||
Register the server with Claude Code:
|
||||
<div class="mcp-code-row">
|
||||
<pre class="mcp-code">{{ claudeCodeCommand }}</pre>
|
||||
<button class="btn btn-secondary btn-sm" @click="copySnippet(claudeCodeCommand, 'cc-add')">
|
||||
{{ copiedSnippetKey === 'cc-add' ? 'Copied' : 'Copy' }}
|
||||
</button>
|
||||
</div>
|
||||
<p class="mcp-hint">
|
||||
<code>--scope user</code> makes <code>fable</code> available across all your projects. Use <code>--scope project</code> to write it into the current repo's <code>.mcp.json</code> instead, or <code>--scope local</code> for this machine and repo only.
|
||||
</p>
|
||||
</li>
|
||||
<li>
|
||||
Verify the connection by running <code>/mcp</code> inside Claude Code — <code>fable</code> should appear as connected.
|
||||
</li>
|
||||
</ol>
|
||||
|
||||
<div class="mcp-install-steps">
|
||||
<h3>Installation</h3>
|
||||
|
||||
<div class="mcp-client-tabs" role="tablist">
|
||||
<button
|
||||
type="button"
|
||||
role="tab"
|
||||
:aria-selected="mcpClientTab === 'claude-code'"
|
||||
:class="['mcp-client-tab', { active: mcpClientTab === 'claude-code' }]"
|
||||
@click="mcpClientTab = 'claude-code'"
|
||||
>Claude Code</button>
|
||||
<button
|
||||
type="button"
|
||||
role="tab"
|
||||
:aria-selected="mcpClientTab === 'claude-desktop'"
|
||||
:class="['mcp-client-tab', { active: mcpClientTab === 'claude-desktop' }]"
|
||||
@click="mcpClientTab = 'claude-desktop'"
|
||||
>Claude Desktop</button>
|
||||
<button
|
||||
type="button"
|
||||
role="tab"
|
||||
:aria-selected="mcpClientTab === 'other'"
|
||||
:class="['mcp-client-tab', { active: mcpClientTab === 'other' }]"
|
||||
@click="mcpClientTab = 'other'"
|
||||
>Other</button>
|
||||
</div>
|
||||
|
||||
<!-- Claude Code tab -->
|
||||
<ol v-if="mcpClientTab === 'claude-code'">
|
||||
<li>
|
||||
Download the wheel above and install it with <a href="https://docs.astral.sh/uv/" target="_blank" rel="noopener">uv</a> or <a href="https://pipx.pypa.io/" target="_blank" rel="noopener">pipx</a> — either one works:
|
||||
<div class="mcp-code-row">
|
||||
<pre class="mcp-code">uv tool install ./{{ mcpInfo.filename }}</pre>
|
||||
<button class="btn btn-secondary btn-sm" @click="copySnippet(`uv tool install ./${mcpInfo.filename}`, 'cc-install-uv')">
|
||||
{{ copiedSnippetKey === 'cc-install-uv' ? 'Copied' : 'Copy' }}
|
||||
</button>
|
||||
</div>
|
||||
<div class="mcp-code-row">
|
||||
<pre class="mcp-code">pipx install ./{{ mcpInfo.filename }}</pre>
|
||||
<button class="btn btn-secondary btn-sm" @click="copySnippet(`pipx install ./${mcpInfo.filename}`, 'cc-install-pipx')">
|
||||
{{ copiedSnippetKey === 'cc-install-pipx' ? 'Copied' : 'Copy' }}
|
||||
</button>
|
||||
</div>
|
||||
<p class="mcp-hint">This puts <code>fable-mcp</code> on your PATH so Claude Code can launch it.</p>
|
||||
</li>
|
||||
<li>
|
||||
Register the server with Claude Code:
|
||||
<div class="mcp-code-row">
|
||||
<pre class="mcp-code">{{ claudeCodeCommand }}</pre>
|
||||
<button class="btn btn-secondary btn-sm" @click="copySnippet(claudeCodeCommand, 'cc-add')">
|
||||
{{ copiedSnippetKey === 'cc-add' ? 'Copied' : 'Copy' }}
|
||||
</button>
|
||||
</div>
|
||||
<p class="mcp-hint">
|
||||
<code>--scope user</code> makes <code>fable</code> available across all your projects. Use <code>--scope project</code> to write it into the current repo's <code>.mcp.json</code> instead, or <code>--scope local</code> for this machine and repo only.
|
||||
<span v-if="!newKeyValue"> Generate an API key above to pre-fill the command.</span>
|
||||
</p>
|
||||
</li>
|
||||
<li>
|
||||
Verify the connection by running <code>/mcp</code> inside Claude Code — <code>fable</code> should appear as connected.
|
||||
</li>
|
||||
</ol>
|
||||
|
||||
<!-- Claude Desktop tab -->
|
||||
<ol v-else-if="mcpClientTab === 'claude-desktop'">
|
||||
<li>
|
||||
Download the wheel above and install it with uv or pipx:
|
||||
<div class="mcp-code-row">
|
||||
<pre class="mcp-code">uv tool install ./{{ mcpInfo.filename }}</pre>
|
||||
<button class="btn btn-secondary btn-sm" @click="copySnippet(`uv tool install ./${mcpInfo.filename}`, 'cd-install')">
|
||||
{{ copiedSnippetKey === 'cd-install' ? 'Copied' : 'Copy' }}
|
||||
</button>
|
||||
</div>
|
||||
</li>
|
||||
<li>
|
||||
Add this block to your Claude Desktop MCP config file (<code>~/Library/Application Support/Claude/claude_desktop_config.json</code> on macOS, <code>%APPDATA%\Claude\claude_desktop_config.json</code> on Windows):
|
||||
<div class="mcp-code-row">
|
||||
<pre class="mcp-code">{{ mcpConfigSnippet }}</pre>
|
||||
<button class="btn btn-secondary btn-sm" @click="copySnippet(mcpConfigSnippet, 'cd-config')">
|
||||
{{ copiedSnippetKey === 'cd-config' ? 'Copied' : 'Copy' }}
|
||||
</button>
|
||||
</div>
|
||||
</li>
|
||||
<li>
|
||||
Restart Claude Desktop. The Fable tools should appear in the available tools list.
|
||||
</li>
|
||||
</ol>
|
||||
|
||||
<!-- Other tab -->
|
||||
<div v-else class="mcp-other">
|
||||
<p>
|
||||
Any MCP-compatible client can launch <code>fable-mcp</code> over stdio. The exact setup depends on the client, but in general you'll need:
|
||||
</p>
|
||||
<ul class="mcp-plain-list">
|
||||
<li>Install the wheel: <code>uv tool install ./{{ mcpInfo.filename }}</code> or <code>pipx install ./{{ mcpInfo.filename }}</code></li>
|
||||
<li>Command: <code>fable-mcp</code></li>
|
||||
<li>Transport: <code>stdio</code></li>
|
||||
<li>Environment variables:
|
||||
<div class="mcp-code-row">
|
||||
<pre class="mcp-code">FABLE_URL={{ origin }}
|
||||
FABLE_API_KEY={{ effectiveApiKey }}</pre>
|
||||
<button class="btn btn-secondary btn-sm" @click="copySnippet(`FABLE_URL=${origin}\nFABLE_API_KEY=${effectiveApiKey}`, 'other-env')">
|
||||
{{ copiedSnippetKey === 'other-env' ? 'Copied' : 'Copy' }}
|
||||
</button>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
<p class="mcp-hint">Consult your MCP client's documentation for how to register a stdio server. These instructions have only been tested with Claude Code.</p>
|
||||
</div>
|
||||
<!-- Claude Desktop tab -->
|
||||
<ol v-else-if="mcpClientTab === 'claude-desktop'">
|
||||
<li>
|
||||
Add this block to your Claude Desktop MCP config file
|
||||
(<code>~/Library/Application Support/Claude/claude_desktop_config.json</code> on macOS,
|
||||
<code>%APPDATA%\Claude\claude_desktop_config.json</code> on Windows):
|
||||
<div class="mcp-code-row">
|
||||
<pre class="mcp-code">{{ mcpConfigSnippet }}</pre>
|
||||
<button class="btn btn-secondary btn-sm" @click="copySnippet(mcpConfigSnippet, 'cd-config')">
|
||||
{{ copiedSnippetKey === 'cd-config' ? 'Copied' : 'Copy' }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="mcp-unavailable">
|
||||
<p>The Fable MCP package is not bundled in this image build. It will be available after the next image rebuild.</p>
|
||||
</div>
|
||||
</template>
|
||||
</li>
|
||||
<li>
|
||||
Restart Claude Desktop. The Fable tools should appear in the available tools list.
|
||||
</li>
|
||||
</ol>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
@@ -4302,6 +4214,13 @@ FABLE_API_KEY={{ effectiveApiKey }}</pre>
|
||||
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;
|
||||
|
||||
Reference in New Issue
Block a user