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