From 1d82e81527404a82bdca2417f1f388df88ebe158 Mon Sep 17 00:00:00 2001 From: Bryan Van Deusen Date: Wed, 10 Jun 2026 00:17:05 -0400 Subject: [PATCH 1/3] feat(plugin): admin-configurable marketplace URL as the install default MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Settings install command had a placeholder — not copyable. Add an instance-global 'plugin_marketplace_url' setting (admin sets it to the app's own repo) that every user's MCP Access reads, so the /plugin marketplace add command is copyable out of the box. Keeps it universal (each deployment configures its own repo) rather than hardcoding one. - services/settings.get_admin_setting(key): admin-scoped global read. - routes/plugin: GET /api/plugin/marketplace-url (any user) + PUT (admin). - SettingsView: Admin → 'Plugin marketplace' field to set it; MCP Access marketplace field falls back to the configured value. Co-Authored-By: Claude Opus 4.8 (1M context) --- frontend/src/views/SettingsView.vue | 66 ++++++++++++++++++++++++++++- src/scribe/routes/plugin.py | 30 ++++++++++++- src/scribe/services/settings.py | 18 ++++++++ 3 files changed, 112 insertions(+), 2 deletions(-) diff --git a/frontend/src/views/SettingsView.vue b/frontend/src/views/SettingsView.vue index 63c107a..7b5115b 100644 --- a/frontend/src/views/SettingsView.vue +++ b/frontend/src/views/SettingsView.vue @@ -144,8 +144,16 @@ const claudeCodeCommand = computed(() => { const _MKT_KEY = 'plugin_marketplace_url'; const pluginMarketplaceUrl = ref(localStorage.getItem(_MKT_KEY) || ''); watch(pluginMarketplaceUrl, (v) => localStorage.setItem(_MKT_KEY, (v || '').trim())); +// Instance default, set by an admin (Admin tab) and loaded on mount. Used when +// the per-browser field is blank so the install command is copyable out of the box. +const serverMarketplaceUrl = ref(''); +const adminMarketplaceUrl = ref(''); +const savingMarketplaceUrl = ref(false); +const marketplaceUrlSaved = ref(false); const pluginInstallCommands = computed(() => { - const mkt = (pluginMarketplaceUrl.value || '').trim() || '.git'; + const mkt = (pluginMarketplaceUrl.value || '').trim() + || serverMarketplaceUrl.value + || '.git'; return `/plugin marketplace add ${mkt}\n/plugin install scribe@scribe-plugin`; }); @@ -429,6 +437,17 @@ onMounted(async () => { searxngConfigured.value = false; } + // Plugin marketplace URL (instance default; readable by all users so the + // install command in MCP Access is copyable). + try { + const mk = await apiGet<{ marketplace_url: string }>("/api/plugin/marketplace-url"); + serverMarketplaceUrl.value = mk.marketplace_url || ""; + adminMarketplaceUrl.value = mk.marketplace_url || ""; + if (!pluginMarketplaceUrl.value) pluginMarketplaceUrl.value = mk.marketplace_url || ""; + } catch { + // not configured yet + } + // Load admin settings if (authStore.isAdmin) { try { @@ -658,6 +677,26 @@ async function saveBaseUrl() { } } +async function saveMarketplaceUrl() { + savingMarketplaceUrl.value = true; + marketplaceUrlSaved.value = false; + try { + const url = adminMarketplaceUrl.value.trim(); + await apiPut("/api/plugin/marketplace-url", { marketplace_url: url }); + serverMarketplaceUrl.value = url; + // Reflect the new instance default in the copyable field unless the user + // set their own per-browser override. + if (!localStorage.getItem(_MKT_KEY)) pluginMarketplaceUrl.value = url; + marketplaceUrlSaved.value = true; + setTimeout(() => (marketplaceUrlSaved.value = false), 2000); + } catch (e) { + const body = (e as { body?: { error?: string } }).body; + toastStore.show(body?.error || "Failed to save marketplace URL", "error"); + } finally { + savingMarketplaceUrl.value = false; + } +} + async function handleRestoreFile(event: Event) { const file = (event.target as HTMLInputElement).files?.[0]; if (!file) return; @@ -1663,6 +1702,31 @@ function formatUserDate(iso: string): string { +
+

Plugin marketplace

+

+ Git URL of the repo that ships this Scribe plugin (usually this app's own repo). + Shown to every user in MCP Access so the install command is + copyable. Example: https://git.example.com/you/Scribe.git +

+
+ + +
+
+ + Saved! +
+
+

Email / SMTP

Configure SMTP to enable email notifications for all users.

diff --git a/src/scribe/routes/plugin.py b/src/scribe/routes/plugin.py index f6c1af0..1d34dca 100644 --- a/src/scribe/routes/plugin.py +++ b/src/scribe/routes/plugin.py @@ -10,11 +10,17 @@ from __future__ import annotations from quart import Blueprint, g, jsonify, request -from scribe.auth import login_required +from scribe.auth import admin_required, get_current_user_id, login_required from scribe.services import plugin_context as plugin_ctx_svc +from scribe.services.settings import get_admin_setting, set_setting plugin_bp = Blueprint("plugin", __name__, url_prefix="/api/plugin") +# Setting key for the git URL of the marketplace that serves this plugin (the +# Scribe app's own repo). Instance-global (stored on an admin account) so every +# user's Settings page shows the real, copyable install command. +_MARKETPLACE_KEY = "plugin_marketplace_url" + @plugin_bp.get("/context") @login_required @@ -30,3 +36,25 @@ async def session_context(): project_id = 0 result = await plugin_ctx_svc.build_session_context(g.user.id, project_id) return jsonify(result) + + +@plugin_bp.get("/marketplace-url") +@login_required +async def get_marketplace_url(): + """The plugin marketplace git URL, readable by any logged-in user so their + Settings page can show a copyable install command.""" + return jsonify({"marketplace_url": await get_admin_setting(_MARKETPLACE_KEY)}) + + +@plugin_bp.put("/marketplace-url") +@admin_required +async def set_marketplace_url(): + """Admin-set the marketplace git URL (e.g. this app's own repo).""" + data = await request.get_json() or {} + url = (data.get("marketplace_url") or "").strip() + if url: + scheme = url.split("://")[0].lower() if "://" in url else "" + if scheme not in ("http", "https"): + return jsonify({"error": "Marketplace URL must use http or https"}), 400 + await set_setting(get_current_user_id(), _MARKETPLACE_KEY, url) + return jsonify({"status": "ok"}) diff --git a/src/scribe/services/settings.py b/src/scribe/services/settings.py index 9502944..20dcbbc 100644 --- a/src/scribe/services/settings.py +++ b/src/scribe/services/settings.py @@ -4,10 +4,28 @@ from sqlalchemy import delete as sa_delete, select from scribe.models import async_session from scribe.models.setting import Setting +from scribe.models.user import User logger = logging.getLogger(__name__) +async def get_admin_setting(key: str, default: str = "") -> str: + """Read an instance-global setting (one stored on an admin account). + + Used for settings that aren't per-user — e.g. the plugin marketplace URL + shown to everyone in Settings. Mirrors the admin-scoped lookup used for + the application base URL. + """ + async with async_session() as session: + result = await session.execute( + select(Setting) + .join(User, Setting.user_id == User.id) + .where(User.role == "admin", Setting.key == key) + ) + setting = result.scalars().first() + return setting.value if setting and setting.value else default + + async def get_setting(user_id: int, key: str, default: str = "") -> str: async with async_session() as session: result = await session.execute( From 559f70eef01d370be285d836e794276339cc9987 Mon Sep 17 00:00:00 2001 From: Bryan Van Deusen Date: Wed, 10 Jun 2026 00:24:06 -0400 Subject: [PATCH 2/3] fix(plugin): inline mcpServers in plugin.json so the MCP server actually loads MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit /reload-plugins reported '0 plugin MCP servers'. Root cause: plugin.json had "mcpServers": "./.mcp.json" — a string path, which is neither a valid inline object nor a recognized reference (per docs, plugin MCP servers are a root .mcp.json OR an inline object in plugin.json), so it parsed to zero servers. Inline the mcpServers object directly in plugin.json and remove the separate .mcp.json. The user_config substitution syntax was already correct (plugins-reference: values substitute as ${user_config.KEY} in MCP configs). Co-Authored-By: Claude Opus 4.8 (1M context) --- plugin/.claude-plugin/plugin.json | 8 +++++++- plugin/README.md | 2 +- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/plugin/.claude-plugin/plugin.json b/plugin/.claude-plugin/plugin.json index f5afc61..9615574 100644 --- a/plugin/.claude-plugin/plugin.json +++ b/plugin/.claude-plugin/plugin.json @@ -3,7 +3,13 @@ "description": "Scribe second brain for Claude Code: MCP tools over your notes/tasks/projects/rules, a session-start push channel that surfaces your always-on rules + active-project context, and a set of universal process-skills (brainstorm, debug, TDD, plan, verify). Replaces superpowers + file-memory with one app-backed plugin.", "version": "0.1.0", "author": { "name": "Bryan Van Deusen" }, - "mcpServers": "./.mcp.json", + "mcpServers": { + "scribe": { + "type": "http", + "url": "${user_config.api_endpoint}/mcp", + "headers": { "Authorization": "Bearer ${user_config.api_token}" } + } + }, "userConfig": { "api_endpoint": { "type": "string", diff --git a/plugin/README.md b/plugin/README.md index 77d6104..8b3ed7c 100644 --- a/plugin/README.md +++ b/plugin/README.md @@ -33,7 +33,7 @@ On install you'll be asked for: ## What gets wired -- `.mcp.json` → the `scribe` MCP server at `/mcp` (Bearer auth). +- `plugin.json` `mcpServers` → the `scribe` MCP server at `/mcp` (Bearer auth). - `hooks/hooks.json` → SessionStart hook (`hooks/scribe_session_context.sh`), **fail-open**: if Scribe is unreachable it injects nothing and never blocks the session. From ff91948fa3b998285b91e46c2c109b51319d7713 Mon Sep 17 00:00:00 2001 From: Bryan Van Deusen Date: Wed, 10 Jun 2026 00:25:09 -0400 Subject: [PATCH 3/3] chore(plugin): bump to 0.1.1 so clients pick up the mcpServers fix Co-Authored-By: Claude Opus 4.8 (1M context) --- plugin/.claude-plugin/plugin.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugin/.claude-plugin/plugin.json b/plugin/.claude-plugin/plugin.json index 9615574..9b1c34d 100644 --- a/plugin/.claude-plugin/plugin.json +++ b/plugin/.claude-plugin/plugin.json @@ -1,7 +1,7 @@ { "name": "scribe", "description": "Scribe second brain for Claude Code: MCP tools over your notes/tasks/projects/rules, a session-start push channel that surfaces your always-on rules + active-project context, and a set of universal process-skills (brainstorm, debug, TDD, plan, verify). Replaces superpowers + file-memory with one app-backed plugin.", - "version": "0.1.0", + "version": "0.1.1", "author": { "name": "Bryan Van Deusen" }, "mcpServers": { "scribe": {