feat(settings): tabbed MCP install instructions with Claude Code flow

Replace the hand-edit-JSON instructions in Settings → API Keys with a
tabbed UI (Claude Code / Claude Desktop / Other). The Claude Code tab
leads with the `claude mcp add` command, pre-filled with FABLE_URL and
the most recently generated API key, plus copy-to-clipboard buttons on
every snippet. Recommend `uv tool install` or `pipx install` over bare
`pip install` so fable-mcp reliably lands on PATH under PEP 668.

Also fix incorrect priority values in fable-mcp tool docstrings — the
enum is `none|low|medium|high`, not `low|normal|high`.

DRY pass: extract shared `copyToClipboard()` helper used by both
`copyApiKey` and the new snippet buttons; reuse existing
`btn btn-secondary btn-sm` for the copy buttons instead of a bespoke
class.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Bryan Van Deusen
2026-04-10 12:35:50 -04:00
parent c1bc73da8e
commit 102c0b74a0
3 changed files with 195 additions and 31 deletions
+3 -3
View File
@@ -27,7 +27,7 @@ The hierarchy is: Project → Milestone → Task/Note.
- **Tasks** belong to a project and optionally a milestone. They support sub-tasks via `parent_id`.
- Status values: `todo`, `in_progress`, `done`, `cancelled`
- Priority values: `low`, `normal`, `high`
- Priority values: `none` (default), `low`, `medium`, `high`
- **Notes** are free-form markdown documents. They can belong to a project or be standalone
(orphan notes). Orphan notes are included in the default RAG scope for chat conversations.
@@ -241,7 +241,7 @@ async def fable_create_task(
title: Task title (required).
body: Markdown description / notes for the task.
status: Initial status — one of: todo (default), in_progress, done, cancelled.
priority: One of: low, normal, high. Omit for no priority.
priority: One of: low, medium, high. Omit for no priority (defaults to "none").
project_id: Associate with a project (0 = no project).
milestone_id: Place within a project milestone (0 = no milestone).
parent_id: Make this a sub-task of another task (0 = top-level).
@@ -280,7 +280,7 @@ async def fable_update_task(
title: New title, or omit to leave unchanged.
body: New markdown body, or omit to leave unchanged.
status: New status — one of: todo, in_progress, done, cancelled.
priority: New priority — one of: low, normal, high.
priority: New priority — one of: none, low, medium, high.
project_id: New project (0 = remove from project). Omit to leave unchanged.
milestone_id: New milestone (0 = remove from milestone). Omit to leave unchanged.
"""
+1 -1
View File
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
[project]
name = "fable-mcp"
version = "0.2.4"
version = "0.2.5"
description = "MCP server for Fabled Assistant"
requires-python = ">=3.12"
dependencies = [
+191 -27
View File
@@ -1,5 +1,5 @@
<script setup lang="ts">
import { ref, watch, onMounted } from "vue";
import { ref, computed, watch, onMounted } from "vue";
import { useSettingsStore } from "@/stores/settings";
import { useAuthStore } from "@/stores/auth";
import { useToastStore } from "@/stores/toast";
@@ -94,17 +94,54 @@ const mcpInfo = ref<{ available: boolean; filename: string | null } | null>(null
const mcpInfoLoading = ref(false);
const origin = window.location.origin;
const mcpConfigSnippet = JSON.stringify({
const mcpClientTab = ref<'claude-code' | 'claude-desktop' | 'other'>('claude-code');
const copiedSnippetKey = ref<string | null>(null);
const effectiveApiKey = computed(() => newKeyValue.value || '<your-api-key>');
const claudeCodeCommand = computed(() => {
return `claude mcp add --transport stdio --scope user fable \\
--env FABLE_URL=${origin} \\
--env FABLE_API_KEY=${effectiveApiKey.value} \\
-- fable-mcp`;
});
const mcpConfigSnippet = computed(() => JSON.stringify({
mcpServers: {
fable: {
command: "fable-mcp",
env: {
FABLE_URL: window.location.origin,
FABLE_API_KEY: "<your-api-key>",
FABLE_URL: origin,
FABLE_API_KEY: effectiveApiKey.value,
},
},
},
}, null, 2);
}, null, 2));
async function copyToClipboard(text: string) {
try {
await navigator.clipboard.writeText(text);
} catch {
// Fallback for http (non-secure) contexts where clipboard API is unavailable
const ta = document.createElement('textarea');
ta.value = text;
ta.style.position = 'fixed';
ta.style.opacity = '0';
document.body.appendChild(ta);
ta.focus();
ta.select();
document.execCommand('copy');
document.body.removeChild(ta);
}
}
async function copySnippet(text: string, key: string) {
await copyToClipboard(text);
copiedSnippetKey.value = key;
setTimeout(() => {
if (copiedSnippetKey.value === key) copiedSnippetKey.value = null;
}, 2000);
}
async function loadMcpInfo() {
if (mcpInfo.value !== null) return;
@@ -142,20 +179,7 @@ async function revokeApiKey(id: number) {
}
async function copyApiKey() {
try {
await navigator.clipboard.writeText(newKeyValue.value);
} catch {
// Fallback for http (non-secure) contexts where clipboard API is unavailable
const ta = document.createElement('textarea');
ta.value = newKeyValue.value;
ta.style.position = 'fixed';
ta.style.opacity = '0';
document.body.appendChild(ta);
ta.focus();
ta.select();
document.execCommand('copy');
document.body.removeChild(ta);
}
await copyToClipboard(newKeyValue.value);
apiKeyCopied.value = true;
setTimeout(() => { apiKeyCopied.value = false; }, 2000);
}
@@ -2529,21 +2553,113 @@ function formatUserDate(iso: string): string {
<div class="mcp-install-steps">
<h3>Installation</h3>
<ol>
<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:
<pre class="mcp-code">pip install {{ mcpInfo.filename }}</pre>
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>
Create a <code>.env</code> file (or set environment variables):
<pre class="mcp-code">FABLE_URL={{ origin }}
FABLE_API_KEY=&lt;your-api-key&gt;</pre>
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>
Add to your Claude MCP config (<code>~/.claude.json</code> or the Claude Desktop config):
<pre class="mcp-code">{{ mcpConfigSnippet }}</pre>
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 v-else class="mcp-unavailable">
@@ -4215,6 +4331,54 @@ FABLE_API_KEY=&lt;your-api-key&gt;</pre>
white-space: pre-wrap;
word-break: break-all;
overflow-x: auto;
flex: 1;
}
.mcp-client-tabs {
display: flex;
gap: 0.25rem;
margin-bottom: 1rem;
border-bottom: 1px solid var(--color-border);
}
.mcp-client-tab {
background: transparent;
border: none;
padding: 0.5rem 0.9rem;
font-size: 0.85rem;
color: var(--color-text-muted);
cursor: pointer;
border-bottom: 2px solid transparent;
margin-bottom: -1px;
transition: color 0.15s, border-color 0.15s;
}
.mcp-client-tab:hover { color: var(--color-text); }
.mcp-client-tab.active {
color: var(--color-primary);
border-bottom-color: var(--color-primary);
font-weight: 600;
}
.mcp-code-row {
display: flex;
align-items: stretch;
gap: 0.4rem;
margin-top: 0.4rem;
}
.mcp-code-row .mcp-code { margin-top: 0; }
.mcp-code-row .btn-sm { white-space: nowrap; }
.mcp-hint {
margin-top: 0.5rem;
font-size: 0.8rem;
opacity: 0.7;
line-height: 1.5;
}
.mcp-other p { font-size: 0.9rem; line-height: 1.6; }
.mcp-plain-list {
list-style: disc;
padding-left: 1.25rem;
display: flex;
flex-direction: column;
gap: 0.5rem;
font-size: 0.9rem;
line-height: 1.6;
}
/* Voice tab */