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 { useSettingsStore } from "@/stores/settings";
|
||||||
import { useAuthStore } from "@/stores/auth";
|
import { useAuthStore } from "@/stores/auth";
|
||||||
import { useToastStore } from "@/stores/toast";
|
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 { usePushStore } from "@/stores/push";
|
||||||
import type { User } from "@/types/auth";
|
import type { User } from "@/types/auth";
|
||||||
import PaginationBar from "@/components/PaginationBar.vue";
|
import PaginationBar from "@/components/PaginationBar.vue";
|
||||||
@@ -93,7 +93,7 @@ function _loadTabContent(tab: string) {
|
|||||||
else if (tab === "groups") loadGroupsPanel();
|
else if (tab === "groups") loadGroupsPanel();
|
||||||
}
|
}
|
||||||
if (tab === "voice") loadVoiceTab();
|
if (tab === "voice") loadVoiceTab();
|
||||||
if (tab === "apikeys") { fetchApiKeys(); loadMcpInfo(); }
|
if (tab === "apikeys") { fetchApiKeys(); }
|
||||||
}
|
}
|
||||||
|
|
||||||
watch(activeTab, (v) => {
|
watch(activeTab, (v) => {
|
||||||
@@ -101,7 +101,7 @@ watch(activeTab, (v) => {
|
|||||||
_loadTabContent(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 apiKeys = ref<ApiKeyEntry[]>([]);
|
||||||
const newKeyName = ref('');
|
const newKeyName = ref('');
|
||||||
const newKeyScope = ref<'read' | 'write'>('write');
|
const newKeyScope = ref<'read' | 'write'>('write');
|
||||||
@@ -109,34 +109,38 @@ const newKeyValue = ref('');
|
|||||||
const apiKeyCopied = ref(false);
|
const apiKeyCopied = ref(false);
|
||||||
const creatingApiKey = ref(false);
|
const creatingApiKey = ref(false);
|
||||||
const revokeConfirmId = ref<number | null>(null);
|
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 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 copiedSnippetKey = ref<string | null>(null);
|
||||||
|
|
||||||
const effectiveApiKey = computed(() => newKeyValue.value || '<your-api-key>');
|
const effectiveApiKey = computed(() => newKeyValue.value || '<your-token>');
|
||||||
|
|
||||||
const claudeCodeCommand = computed(() => {
|
const claudeCodeCommand = computed(() => {
|
||||||
return `claude mcp add --transport stdio --scope user fable \\
|
return `claude mcp add --transport http --scope user fable \\
|
||||||
--env FABLE_URL=${origin} \\
|
--url ${mcpUrl.value} \\
|
||||||
--env FABLE_API_KEY=${effectiveApiKey.value} \\
|
--header "Authorization: Bearer ${effectiveApiKey.value}"`;
|
||||||
-- fable-mcp`;
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const mcpConfigSnippet = computed(() => JSON.stringify({
|
const mcpConfigSnippet = computed(() => JSON.stringify({
|
||||||
mcpServers: {
|
mcpServers: {
|
||||||
fable: {
|
fable: {
|
||||||
command: "fable-mcp",
|
url: mcpUrl.value,
|
||||||
env: {
|
headers: {
|
||||||
FABLE_URL: origin,
|
Authorization: `Bearer ${effectiveApiKey.value}`,
|
||||||
FABLE_API_KEY: effectiveApiKey.value,
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}, null, 2));
|
}, null, 2));
|
||||||
|
|
||||||
|
async function copyMcpUrl() {
|
||||||
|
await copyToClipboard(mcpUrl.value);
|
||||||
|
mcpUrlCopied.value = true;
|
||||||
|
setTimeout(() => { mcpUrlCopied.value = false; }, 2000);
|
||||||
|
}
|
||||||
|
|
||||||
async function copyToClipboard(text: string) {
|
async function copyToClipboard(text: string) {
|
||||||
try {
|
try {
|
||||||
await navigator.clipboard.writeText(text);
|
await navigator.clipboard.writeText(text);
|
||||||
@@ -162,18 +166,6 @@ async function copySnippet(text: string, key: string) {
|
|||||||
}, 2000);
|
}, 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() {
|
async function fetchApiKeys() {
|
||||||
apiKeys.value = await listApiKeys();
|
apiKeys.value = await listApiKeys();
|
||||||
}
|
}
|
||||||
@@ -203,38 +195,6 @@ async function copyApiKey() {
|
|||||||
setTimeout(() => { apiKeyCopied.value = false; }, 2000);
|
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
|
// Groups management
|
||||||
const groups = ref<GroupEntry[]>([]);
|
const groups = ref<GroupEntry[]>([]);
|
||||||
const groupsLoading = ref(false);
|
const groupsLoading = ref(false);
|
||||||
@@ -1501,7 +1461,7 @@ function formatUserDate(iso: string): string {
|
|||||||
:class="['sidebar-item', { active: activeTab === tab }]"
|
:class="['sidebar-item', { active: activeTab === tab }]"
|
||||||
@click="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>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="authStore.isAdmin" class="sidebar-group">
|
<div v-if="authStore.isAdmin" class="sidebar-group">
|
||||||
@@ -2514,42 +2474,64 @@ function formatUserDate(iso: string): string {
|
|||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- ── API Keys ── -->
|
<!-- ── MCP Access ── -->
|
||||||
<div v-show="activeTab === 'apikeys'" class="settings-grid">
|
<div v-show="activeTab === 'apikeys'" class="settings-grid">
|
||||||
|
<!-- Endpoint URL -->
|
||||||
<section class="settings-section full-width">
|
<section class="settings-section full-width">
|
||||||
<h2>API Keys</h2>
|
<h2>MCP Access</h2>
|
||||||
<p class="settings-description">
|
<p class="settings-description">
|
||||||
API keys let external tools (like the Fable MCP server) access your data without a browser session.
|
Connect Claude (Code or Desktop) to this Fable instance. Claude reads and writes your notes,
|
||||||
Write-scoped keys can create and update content; read-only keys can only query.
|
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>
|
</p>
|
||||||
|
|
||||||
<!-- Create form -->
|
<!-- Create form -->
|
||||||
<div class="api-key-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">
|
<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="read" /> Read-only</label>
|
||||||
<label><input type="radio" v-model="newKeyScope" value="write" /> Read + Write</label>
|
<label><input type="radio" v-model="newKeyScope" value="write" /> Read + Write</label>
|
||||||
</div>
|
</div>
|
||||||
<button @click="createApiKey" :disabled="!newKeyName || creatingApiKey" class="btn btn-primary">
|
<button @click="createApiKey" :disabled="!newKeyName || creatingApiKey" class="btn btn-primary">
|
||||||
{{ creatingApiKey ? 'Generating…' : 'Generate Key' }}
|
{{ creatingApiKey ? 'Generating…' : 'Generate token' }}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- One-time key reveal -->
|
<!-- One-time token reveal -->
|
||||||
<div v-if="newKeyValue" class="api-key-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">
|
<div class="api-key-value-row">
|
||||||
<code class="api-key-value">{{ newKeyValue }}</code>
|
<code class="api-key-value">{{ newKeyValue }}</code>
|
||||||
<button @click="copyApiKey" class="btn btn-secondary btn-sm">{{ apiKeyCopied ? 'Copied!' : 'Copy' }}</button>
|
<button @click="copyApiKey" class="btn btn-secondary btn-sm">{{ apiKeyCopied ? 'Copied!' : 'Copy' }}</button>
|
||||||
</div>
|
</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;">
|
<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>
|
<button @click="newKeyValue = ''" class="btn btn-secondary">Done</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Keys table -->
|
<!-- Tokens table -->
|
||||||
<table v-if="apiKeys.length > 0" class="api-keys-table">
|
<table v-if="apiKeys.length > 0" class="api-keys-table">
|
||||||
<thead>
|
<thead>
|
||||||
<tr><th>Name</th><th>Scope</th><th>Prefix</th><th>Last Used</th><th></th></tr>
|
<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>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</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>
|
</section>
|
||||||
|
|
||||||
<!-- Fable MCP install section -->
|
<!-- Connect Claude -->
|
||||||
<section class="settings-section full-width">
|
<section class="settings-section full-width">
|
||||||
<h2>Fable MCP</h2>
|
<h2>Connect Claude</h2>
|
||||||
<p class="settings-description">
|
<p class="settings-description">
|
||||||
The Fable MCP server lets Claude (and other MCP clients) read and write your notes, tasks, and projects.
|
Run the appropriate snippet for your client. Generate a token above first to pre-fill the
|
||||||
Install it locally, then point it at this Fable instance with an API key above.
|
token value, or copy the snippet now and paste your own where it says <code><your-token></code>.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<div v-if="mcpInfoLoading" class="mcp-status">Checking package availability…</div>
|
<div class="mcp-client-tabs" role="tablist">
|
||||||
<template v-else-if="mcpInfo">
|
<button
|
||||||
<div v-if="mcpInfo.available" class="mcp-available">
|
type="button"
|
||||||
<div class="mcp-pkg-row">
|
role="tab"
|
||||||
<span class="mcp-pkg-name">{{ mcpInfo.filename }}</span>
|
:aria-selected="mcpClientTab === 'claude-code'"
|
||||||
<a href="/api/fable-mcp/download" class="btn btn-primary btn-sm" download>Download</a>
|
: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>
|
</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">
|
<!-- Claude Desktop tab -->
|
||||||
<h3>Installation</h3>
|
<ol v-else-if="mcpClientTab === 'claude-desktop'">
|
||||||
|
<li>
|
||||||
<div class="mcp-client-tabs" role="tablist">
|
Add this block to your Claude Desktop MCP config file
|
||||||
<button
|
(<code>~/Library/Application Support/Claude/claude_desktop_config.json</code> on macOS,
|
||||||
type="button"
|
<code>%APPDATA%\Claude\claude_desktop_config.json</code> on Windows):
|
||||||
role="tab"
|
<div class="mcp-code-row">
|
||||||
:aria-selected="mcpClientTab === 'claude-code'"
|
<pre class="mcp-code">{{ mcpConfigSnippet }}</pre>
|
||||||
:class="['mcp-client-tab', { active: mcpClientTab === 'claude-code' }]"
|
<button class="btn btn-secondary btn-sm" @click="copySnippet(mcpConfigSnippet, 'cd-config')">
|
||||||
@click="mcpClientTab = 'claude-code'"
|
{{ copiedSnippetKey === 'cd-config' ? 'Copied' : 'Copy' }}
|
||||||
>Claude Code</button>
|
</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>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</li>
|
||||||
<div v-else class="mcp-unavailable">
|
<li>
|
||||||
<p>The Fable MCP package is not bundled in this image build. It will be available after the next image rebuild.</p>
|
Restart Claude Desktop. The Fable tools should appear in the available tools list.
|
||||||
</div>
|
</li>
|
||||||
</template>
|
</ol>
|
||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -4302,6 +4214,13 @@ FABLE_API_KEY={{ effectiveApiKey }}</pre>
|
|||||||
border-bottom-color: var(--color-primary);
|
border-bottom-color: var(--color-primary);
|
||||||
font-weight: 500;
|
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 {
|
.mcp-code-row {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: stretch;
|
align-items: stretch;
|
||||||
|
|||||||
Reference in New Issue
Block a user