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`. - **Tasks** belong to a project and optionally a milestone. They support sub-tasks via `parent_id`.
- Status values: `todo`, `in_progress`, `done`, `cancelled` - 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 - **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. (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). title: Task title (required).
body: Markdown description / notes for the task. body: Markdown description / notes for the task.
status: Initial status — one of: todo (default), in_progress, done, cancelled. 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). project_id: Associate with a project (0 = no project).
milestone_id: Place within a project milestone (0 = no milestone). milestone_id: Place within a project milestone (0 = no milestone).
parent_id: Make this a sub-task of another task (0 = top-level). 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. title: New title, or omit to leave unchanged.
body: New markdown body, or omit to leave unchanged. body: New markdown body, or omit to leave unchanged.
status: New status — one of: todo, in_progress, done, cancelled. 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. project_id: New project (0 = remove from project). Omit to leave unchanged.
milestone_id: New milestone (0 = remove from milestone). 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] [project]
name = "fable-mcp" name = "fable-mcp"
version = "0.2.4" version = "0.2.5"
description = "MCP server for Fabled Assistant" description = "MCP server for Fabled Assistant"
requires-python = ">=3.12" requires-python = ">=3.12"
dependencies = [ dependencies = [
+191 -27
View File
@@ -1,5 +1,5 @@
<script setup lang="ts"> <script setup lang="ts">
import { ref, watch, onMounted } from "vue"; 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";
@@ -94,17 +94,54 @@ const mcpInfo = ref<{ available: boolean; filename: string | null } | null>(null
const mcpInfoLoading = ref(false); const mcpInfoLoading = ref(false);
const origin = window.location.origin; 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: { mcpServers: {
fable: { fable: {
command: "fable-mcp", command: "fable-mcp",
env: { env: {
FABLE_URL: window.location.origin, FABLE_URL: origin,
FABLE_API_KEY: "<your-api-key>", 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() { async function loadMcpInfo() {
if (mcpInfo.value !== null) return; if (mcpInfo.value !== null) return;
@@ -142,20 +179,7 @@ async function revokeApiKey(id: number) {
} }
async function copyApiKey() { async function copyApiKey() {
try { await copyToClipboard(newKeyValue.value);
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);
}
apiKeyCopied.value = true; apiKeyCopied.value = true;
setTimeout(() => { apiKeyCopied.value = false; }, 2000); setTimeout(() => { apiKeyCopied.value = false; }, 2000);
} }
@@ -2529,21 +2553,113 @@ function formatUserDate(iso: string): string {
<div class="mcp-install-steps"> <div class="mcp-install-steps">
<h3>Installation</h3> <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> <li>
Download the wheel above and install it: 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:
<pre class="mcp-code">pip install {{ mcpInfo.filename }}</pre> <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>
<li> <li>
Create a <code>.env</code> file (or set environment variables): Register the server with Claude Code:
<pre class="mcp-code">FABLE_URL={{ origin }} <div class="mcp-code-row">
FABLE_API_KEY=&lt;your-api-key&gt;</pre> <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>
<li> <li>
Add to your Claude MCP config (<code>~/.claude.json</code> or the Claude Desktop config): Verify the connection by running <code>/mcp</code> inside Claude Code — <code>fable</code> should appear as connected.
<pre class="mcp-code">{{ mcpConfigSnippet }}</pre>
</li> </li>
</ol> </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> </div>
<div v-else class="mcp-unavailable"> <div v-else class="mcp-unavailable">
@@ -4215,6 +4331,54 @@ FABLE_API_KEY=&lt;your-api-key&gt;</pre>
white-space: pre-wrap; white-space: pre-wrap;
word-break: break-all; word-break: break-all;
overflow-x: auto; 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 */ /* Voice tab */