feat(fable-mcp): build wheel in Docker image, serve download, add admin log tool, and Settings install UI

- Dockerfile: build fable-mcp wheel into /app/dist/ during image build
- routes/fable_mcp_dist.py: GET /api/fable-mcp/info + /download endpoints
- app.py: register fable_mcp_dist_bp
- fable_mcp/tools/admin.py: get_app_logs() hitting /api/admin/logs
- fable_mcp/server.py: fable_get_app_logs MCP tool
- SettingsView: "Fable MCP" section in API Keys tab with download button and install instructions
- client.ts: getFableMcpInfo() helper
- ci.yml: add fable-mcp/** to trigger paths

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-24 17:26:05 -04:00
parent 6564a03c0e
commit 553c38200a
8 changed files with 202 additions and 3 deletions
+100 -2
View File
@@ -3,7 +3,7 @@ import { ref, watch, onMounted } from "vue";
import { useSettingsStore } from "@/stores/settings";
import { useAuthStore } from "@/stores/auth";
import { useToastStore } from "@/stores/toast";
import { apiGet, apiPost, apiPut, apiDelete, listGroups, createGroup, deleteGroup, listGroupMembers, addGroupMember, removeGroupMember, searchUsers, getBriefingConfig, saveBriefingConfig, getBriefingFeeds, createBriefingFeed, deleteBriefingFeed, refreshBriefingFeeds, geocodeAddress, type GroupEntry, type GroupMember, type UserSearchResult, type BriefingConfig, type BriefingFeed } from "@/api/client";
import { apiGet, apiPost, apiPut, apiDelete, listGroups, createGroup, deleteGroup, listGroupMembers, addGroupMember, removeGroupMember, searchUsers, getBriefingConfig, saveBriefingConfig, getBriefingFeeds, createBriefingFeed, deleteBriefingFeed, refreshBriefingFeeds, geocodeAddress, getFableMcpInfo, type GroupEntry, type GroupMember, type UserSearchResult, type BriefingConfig, type BriefingFeed } from "@/api/client";
import { usePushStore } from "@/stores/push";
import type { User } from "@/types/auth";
import PaginationBar from "@/components/PaginationBar.vue";
@@ -48,7 +48,7 @@ watch(activeTab, (v) => {
if (v === "logs" && authStore.isAdmin) loadLogsPanel();
if (v === "groups" && authStore.isAdmin) loadGroupsPanel();
if (v === "briefing") loadBriefingTab();
if (v === "apikeys") fetchApiKeys();
if (v === "apikeys") { fetchApiKeys(); loadMcpInfo(); }
});
// API Keys
@@ -59,6 +59,33 @@ const newKeyValue = ref('');
const apiKeyCopied = ref(false);
const creatingApiKey = ref(false);
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 mcpConfigSnippet = JSON.stringify({
mcpServers: {
fable: {
command: "fable-mcp",
env: {
FABLE_URL: window.location.origin,
FABLE_API_KEY: "<your-api-key>",
},
},
},
}, null, 2);
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() {
const res = await fetch('/api/api-keys', { credentials: 'include' });
@@ -1818,6 +1845,47 @@ function formatUserDate(iso: string): string {
</table>
<p v-else-if="apiKeys.length === 0 && activeTab === 'apikeys'" class="settings-empty">No API keys yet.</p>
</section>
<!-- Fable MCP install section -->
<section class="settings-section full-width">
<h2>Fable MCP</h2>
<p class="settings-description">
The Fable MCP server lets Claude (and other MCP clients) read and write your notes, tasks, and projects.
Install it locally, then point it at this Fable instance with an API key above.
</p>
<div v-if="mcpInfoLoading" class="mcp-status">Checking package availability…</div>
<template v-else-if="mcpInfo">
<div v-if="mcpInfo.available" class="mcp-available">
<div class="mcp-pkg-row">
<span class="mcp-pkg-name">{{ mcpInfo.filename }}</span>
<a href="/api/fable-mcp/download" class="btn btn-primary btn-sm" download>Download</a>
</div>
<div class="mcp-install-steps">
<h3>Installation</h3>
<ol>
<li>
Download the wheel above and install it:
<pre class="mcp-code">pip install {{ mcpInfo.filename }}</pre>
</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>
</li>
<li>
Add to your Claude MCP config (<code>~/.claude.json</code> or the Claude Desktop config):
<pre class="mcp-code">{{ mcpConfigSnippet }}</pre>
</li>
</ol>
</div>
</div>
<div v-else class="mcp-unavailable">
<p>The Fable MCP package is not bundled in this image build. It will be available after the next image rebuild.</p>
</div>
</template>
</section>
</div>
<!-- ── Admin ── -->
@@ -3411,4 +3479,34 @@ function formatUserDate(iso: string): string {
.scope-badge.write { background: color-mix(in srgb, #10b981 15%, transparent); color: #10b981; }
.settings-empty { opacity: 0.5; margin-top: 1rem; }
.settings-description { opacity: 0.7; margin-bottom: 1rem; line-height: 1.5; }
/* Fable MCP section */
.mcp-status { opacity: 0.6; font-size: 0.9rem; }
.mcp-unavailable p { opacity: 0.7; }
.mcp-available { display: flex; flex-direction: column; gap: 1.25rem; }
.mcp-pkg-row {
display: flex;
align-items: center;
gap: 1rem;
padding: 0.65rem 0.9rem;
background: color-mix(in srgb, var(--color-primary) 8%, transparent);
border: 1px solid color-mix(in srgb, var(--color-primary) 25%, transparent);
border-radius: 8px;
}
.mcp-pkg-name { font-family: monospace; font-size: 0.9rem; flex: 1; }
.mcp-install-steps h3 { font-size: 0.95rem; font-weight: 600; margin-bottom: 0.75rem; }
.mcp-install-steps ol { padding-left: 1.25rem; display: flex; flex-direction: column; gap: 0.75rem; }
.mcp-install-steps li { line-height: 1.6; font-size: 0.9rem; }
.mcp-code {
margin-top: 0.4rem;
padding: 0.55rem 0.75rem;
background: color-mix(in srgb, var(--color-text) 6%, transparent);
border: 1px solid var(--color-border);
border-radius: 6px;
font-size: 0.82rem;
font-family: monospace;
white-space: pre-wrap;
word-break: break-all;
overflow-x: auto;
}
</style>