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:
@@ -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.
|
||||
"""
|
||||
|
||||
@@ -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 = [
|
||||
|
||||
@@ -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=<your-api-key></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=<your-api-key></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 */
|
||||
|
||||
Reference in New Issue
Block a user