diff --git a/crates/agent-gateway/web/src/i18n/config.ts b/crates/agent-gateway/web/src/i18n/config.ts index c2cc1daac..65e6f492d 100644 --- a/crates/agent-gateway/web/src/i18n/config.ts +++ b/crates/agent-gateway/web/src/i18n/config.ts @@ -1653,6 +1653,17 @@ export const translations: Record> = { "mcpHub.storePreviewDetailPage": "详情页", "mcpHub.storePreviewHomepage": "主页", "mcpHub.storePreviewRepository": "仓库", + "mcpHub.featuredTitle": "精选连接", + "mcpHub.featuredObsidianDesc": + "连接 Obsidian 笔记库:通过 Local REST API 插件读写笔记、全文检索、追加内容与管理 Vault 文件。", + "mcpHub.featuredObsidianRequirement": + "需先在 Obsidian 中安装并启用 Local REST API 社区插件,且本机可运行 uvx(uv)。", + "mcpHub.featuredObsidianApiKeyLabel": "Obsidian API Key", + "mcpHub.featuredObsidianApiKeyDesc": "在 Obsidian 设置 → Local REST API 面板中复制 API Key。", + "mcpHub.featuredObsidianHostLabel": "Obsidian 地址", + "mcpHub.featuredObsidianHostDesc": "默认 127.0.0.1,连接本机 Obsidian 时无需修改。", + "mcpHub.featuredObsidianPortLabel": "端口", + "mcpHub.featuredObsidianPortDesc": "Local REST API 默认 HTTPS 端口为 27124。", /* ── Settings Skills ── */ "settings.skillsDesc": "为 AI 扩展可调用的技能模块", @@ -3570,6 +3581,18 @@ export const translations: Record> = { "mcpHub.storePreviewDetailPage": "Detail page", "mcpHub.storePreviewHomepage": "Homepage", "mcpHub.storePreviewRepository": "Repository", + "mcpHub.featuredTitle": "Featured connectors", + "mcpHub.featuredObsidianDesc": + "Connect your Obsidian vault: read, write, append, and search notes through the Local REST API plugin.", + "mcpHub.featuredObsidianRequirement": + "Requires the Local REST API community plugin enabled in Obsidian, plus uvx (uv) available on this machine.", + "mcpHub.featuredObsidianApiKeyLabel": "Obsidian API Key", + "mcpHub.featuredObsidianApiKeyDesc": + "Copy the API key from Obsidian Settings → Local REST API.", + "mcpHub.featuredObsidianHostLabel": "Obsidian host", + "mcpHub.featuredObsidianHostDesc": "Defaults to 127.0.0.1 for a local Obsidian.", + "mcpHub.featuredObsidianPortLabel": "Port", + "mcpHub.featuredObsidianPortDesc": "Local REST API HTTPS port, 27124 by default.", /* ── Settings Skills ── */ "settings.skillsDesc": "Extend the AI with callable skill modules", diff --git a/crates/agent-gateway/web/src/lib/mcpRegistry/featured.ts b/crates/agent-gateway/web/src/lib/mcpRegistry/featured.ts new file mode 100644 index 000000000..5cad0c36b --- /dev/null +++ b/crates/agent-gateway/web/src/lib/mcpRegistry/featured.ts @@ -0,0 +1,88 @@ +import type { McpServerConfig } from "../settings"; +import type { McpRegistryCard, McpRegistryConfigInput } from "./index"; + +type TranslateFn = (key: string) => string; + +// 精选连接:本地内置的安装草稿(不发起网络请求),复用 Store 的 +// needs_config → 配置弹窗 → applyMcpRegistryInstallConfig 安装链路。 +// 文案经调用方传入的 t() 取自 i18n,保证 GUI/WebUI 双端一致。 + +const OBSIDIAN_DEFAULT_HOST = "127.0.0.1"; +const OBSIDIAN_DEFAULT_PORT = "27124"; + +function buildObsidianConfigInputs(t: TranslateFn): McpRegistryConfigInput[] { + return [ + { + name: "OBSIDIAN_API_KEY", + label: t("mcpHub.featuredObsidianApiKeyLabel"), + description: t("mcpHub.featuredObsidianApiKeyDesc"), + required: true, + secret: true, + target: "env", + }, + { + name: "OBSIDIAN_HOST", + label: t("mcpHub.featuredObsidianHostLabel"), + description: t("mcpHub.featuredObsidianHostDesc"), + required: false, + secret: false, + target: "env", + }, + { + name: "OBSIDIAN_PORT", + label: t("mcpHub.featuredObsidianPortLabel"), + description: t("mcpHub.featuredObsidianPortDesc"), + required: false, + secret: false, + target: "env", + }, + ]; +} + +function buildObsidianCard(t: TranslateFn): McpRegistryCard { + // Local REST API 插件默认监听 https://127.0.0.1:27124(自签名证书), + // mcp-obsidian 在进程启动时读取这些 env,故 host/port/protocol 预填即可生效。 + const server: McpServerConfig = { + id: "obsidian", + // needs_config 草稿保持禁用,配置弹窗应用 API Key 后由安装链路置为启用。 + enabled: false, + transport: "stdio", + command: "uvx", + args: ["mcp-obsidian"], + env: { + OBSIDIAN_API_KEY: "...", + OBSIDIAN_HOST: OBSIDIAN_DEFAULT_HOST, + OBSIDIAN_PORT: OBSIDIAN_DEFAULT_PORT, + OBSIDIAN_PROTOCOL: "https", + }, + url: "", + timeoutMs: 60_000, + }; + + return { + source: "featured", + id: "featured:obsidian", + sourceId: "obsidian", + name: "mcp-obsidian", + displayName: "Obsidian", + description: t("mcpHub.featuredObsidianDesc"), + homepageUrl: "https://github.com/coddingtonbear/obsidian-local-rest-api", + repositoryUrl: "https://github.com/MarkusPfundstein/mcp-obsidian", + verified: true, + remote: false, + tags: ["notes", "knowledge-base", "local-rest-api"], + transportHints: ["stdio"], + installDraft: { + server, + status: "needs_config", + requiredConfig: buildObsidianConfigInputs(t), + warnings: [t("mcpHub.featuredObsidianRequirement")], + commandPreview: "uvx mcp-obsidian", + }, + detailUrl: "https://github.com/MarkusPfundstein/mcp-obsidian", + }; +} + +export function getFeaturedMcpRegistryCards(t: TranslateFn): McpRegistryCard[] { + return [buildObsidianCard(t)]; +} diff --git a/crates/agent-gateway/web/src/lib/mcpRegistry/index.ts b/crates/agent-gateway/web/src/lib/mcpRegistry/index.ts index 83dc4f539..4406a22bc 100644 --- a/crates/agent-gateway/web/src/lib/mcpRegistry/index.ts +++ b/crates/agent-gateway/web/src/lib/mcpRegistry/index.ts @@ -1,7 +1,8 @@ import { hubFetch } from "../hubFetch"; import type { McpServerConfig } from "../settings"; -export type McpRegistrySource = "official" | "smithery" | "glama"; +// "featured" 是本地内置的精选连接(见 ./featured.ts),不参与 registry 搜索。 +export type McpRegistrySource = "official" | "smithery" | "glama" | "featured"; export type McpRegistryConfigInput = { name: string; @@ -943,3 +944,5 @@ export function applyMcpRegistryInstallConfig( commandPreview: commandPreview(server), }; } + +export { getFeaturedMcpRegistryCards } from "./featured"; diff --git a/crates/agent-gateway/web/src/pages/mcp-hub/McpRegistryBrowser.tsx b/crates/agent-gateway/web/src/pages/mcp-hub/McpRegistryBrowser.tsx index 0c51fba0b..93ad11b4b 100644 --- a/crates/agent-gateway/web/src/pages/mcp-hub/McpRegistryBrowser.tsx +++ b/crates/agent-gateway/web/src/pages/mcp-hub/McpRegistryBrowser.tsx @@ -32,6 +32,7 @@ import { useLocale } from "../../i18n"; import { applyMcpRegistryInstallConfig, createUniqueMcpServerId, + getFeaturedMcpRegistryCards, MCP_REGISTRY_SOURCE_OPTIONS, type McpRegistryCard, type McpRegistryConfigInput, @@ -1292,6 +1293,9 @@ export function McpRegistryBrowser(props: McpRegistryBrowserProps) { const [previewError, setPreviewError] = useState(null); const [installedByCardId, setInstalledByCardId] = useState>({}); const groupedItems = useMemo(() => groupMcpRegistryCards(items), [items]); + // 本地内置的精选连接(如 Obsidian),仅在未搜索时展示,避免挤占搜索结果。 + const featuredGroups = useMemo(() => groupMcpRegistryCards(getFeaturedMcpRegistryCards(t)), [t]); + const showFeatured = query.trim() === ""; const existingIds = useMemo( () => new Set(settings.mcp.servers.map((server) => server.id)), @@ -1504,6 +1508,26 @@ export function McpRegistryBrowser(props: McpRegistryBrowserProps) {
+ {showFeatured && featuredGroups.length > 0 ? ( +
+
+ + {t("mcpHub.featuredTitle")} +
+
+ {featuredGroups.map((group) => ( + void installCard(next)} + /> + ))} +
+
+ ) : null} {loading && items.length === 0 ? ( <>
diff --git a/crates/agent-gui/src/i18n/config.ts b/crates/agent-gui/src/i18n/config.ts index af79b6d4f..908f05b46 100644 --- a/crates/agent-gui/src/i18n/config.ts +++ b/crates/agent-gui/src/i18n/config.ts @@ -1716,6 +1716,17 @@ export const translations: Record> = { "mcpHub.storePreviewDetailPage": "详情页", "mcpHub.storePreviewHomepage": "主页", "mcpHub.storePreviewRepository": "仓库", + "mcpHub.featuredTitle": "精选连接", + "mcpHub.featuredObsidianDesc": + "连接 Obsidian 笔记库:通过 Local REST API 插件读写笔记、全文检索、追加内容与管理 Vault 文件。", + "mcpHub.featuredObsidianRequirement": + "需先在 Obsidian 中安装并启用 Local REST API 社区插件,且本机可运行 uvx(uv)。", + "mcpHub.featuredObsidianApiKeyLabel": "Obsidian API Key", + "mcpHub.featuredObsidianApiKeyDesc": "在 Obsidian 设置 → Local REST API 面板中复制 API Key。", + "mcpHub.featuredObsidianHostLabel": "Obsidian 地址", + "mcpHub.featuredObsidianHostDesc": "默认 127.0.0.1,连接本机 Obsidian 时无需修改。", + "mcpHub.featuredObsidianPortLabel": "端口", + "mcpHub.featuredObsidianPortDesc": "Local REST API 默认 HTTPS 端口为 27124。", /* ── Settings Skills ── */ "settings.skillsDesc": "为 AI 扩展可调用的技能模块", @@ -3722,6 +3733,18 @@ export const translations: Record> = { "mcpHub.storePreviewDetailPage": "Detail page", "mcpHub.storePreviewHomepage": "Homepage", "mcpHub.storePreviewRepository": "Repository", + "mcpHub.featuredTitle": "Featured connectors", + "mcpHub.featuredObsidianDesc": + "Connect your Obsidian vault: read, write, append, and search notes through the Local REST API plugin.", + "mcpHub.featuredObsidianRequirement": + "Requires the Local REST API community plugin enabled in Obsidian, plus uvx (uv) available on this machine.", + "mcpHub.featuredObsidianApiKeyLabel": "Obsidian API Key", + "mcpHub.featuredObsidianApiKeyDesc": + "Copy the API key from Obsidian Settings → Local REST API.", + "mcpHub.featuredObsidianHostLabel": "Obsidian host", + "mcpHub.featuredObsidianHostDesc": "Defaults to 127.0.0.1 for a local Obsidian.", + "mcpHub.featuredObsidianPortLabel": "Port", + "mcpHub.featuredObsidianPortDesc": "Local REST API HTTPS port, 27124 by default.", /* ── Settings Skills ── */ "settings.skillsDesc": "Extend the AI with callable skill modules", diff --git a/crates/agent-gui/src/lib/mcpRegistry/featured.ts b/crates/agent-gui/src/lib/mcpRegistry/featured.ts new file mode 100644 index 000000000..5cad0c36b --- /dev/null +++ b/crates/agent-gui/src/lib/mcpRegistry/featured.ts @@ -0,0 +1,88 @@ +import type { McpServerConfig } from "../settings"; +import type { McpRegistryCard, McpRegistryConfigInput } from "./index"; + +type TranslateFn = (key: string) => string; + +// 精选连接:本地内置的安装草稿(不发起网络请求),复用 Store 的 +// needs_config → 配置弹窗 → applyMcpRegistryInstallConfig 安装链路。 +// 文案经调用方传入的 t() 取自 i18n,保证 GUI/WebUI 双端一致。 + +const OBSIDIAN_DEFAULT_HOST = "127.0.0.1"; +const OBSIDIAN_DEFAULT_PORT = "27124"; + +function buildObsidianConfigInputs(t: TranslateFn): McpRegistryConfigInput[] { + return [ + { + name: "OBSIDIAN_API_KEY", + label: t("mcpHub.featuredObsidianApiKeyLabel"), + description: t("mcpHub.featuredObsidianApiKeyDesc"), + required: true, + secret: true, + target: "env", + }, + { + name: "OBSIDIAN_HOST", + label: t("mcpHub.featuredObsidianHostLabel"), + description: t("mcpHub.featuredObsidianHostDesc"), + required: false, + secret: false, + target: "env", + }, + { + name: "OBSIDIAN_PORT", + label: t("mcpHub.featuredObsidianPortLabel"), + description: t("mcpHub.featuredObsidianPortDesc"), + required: false, + secret: false, + target: "env", + }, + ]; +} + +function buildObsidianCard(t: TranslateFn): McpRegistryCard { + // Local REST API 插件默认监听 https://127.0.0.1:27124(自签名证书), + // mcp-obsidian 在进程启动时读取这些 env,故 host/port/protocol 预填即可生效。 + const server: McpServerConfig = { + id: "obsidian", + // needs_config 草稿保持禁用,配置弹窗应用 API Key 后由安装链路置为启用。 + enabled: false, + transport: "stdio", + command: "uvx", + args: ["mcp-obsidian"], + env: { + OBSIDIAN_API_KEY: "...", + OBSIDIAN_HOST: OBSIDIAN_DEFAULT_HOST, + OBSIDIAN_PORT: OBSIDIAN_DEFAULT_PORT, + OBSIDIAN_PROTOCOL: "https", + }, + url: "", + timeoutMs: 60_000, + }; + + return { + source: "featured", + id: "featured:obsidian", + sourceId: "obsidian", + name: "mcp-obsidian", + displayName: "Obsidian", + description: t("mcpHub.featuredObsidianDesc"), + homepageUrl: "https://github.com/coddingtonbear/obsidian-local-rest-api", + repositoryUrl: "https://github.com/MarkusPfundstein/mcp-obsidian", + verified: true, + remote: false, + tags: ["notes", "knowledge-base", "local-rest-api"], + transportHints: ["stdio"], + installDraft: { + server, + status: "needs_config", + requiredConfig: buildObsidianConfigInputs(t), + warnings: [t("mcpHub.featuredObsidianRequirement")], + commandPreview: "uvx mcp-obsidian", + }, + detailUrl: "https://github.com/MarkusPfundstein/mcp-obsidian", + }; +} + +export function getFeaturedMcpRegistryCards(t: TranslateFn): McpRegistryCard[] { + return [buildObsidianCard(t)]; +} diff --git a/crates/agent-gui/src/lib/mcpRegistry/index.ts b/crates/agent-gui/src/lib/mcpRegistry/index.ts index 83dc4f539..4406a22bc 100644 --- a/crates/agent-gui/src/lib/mcpRegistry/index.ts +++ b/crates/agent-gui/src/lib/mcpRegistry/index.ts @@ -1,7 +1,8 @@ import { hubFetch } from "../hubFetch"; import type { McpServerConfig } from "../settings"; -export type McpRegistrySource = "official" | "smithery" | "glama"; +// "featured" 是本地内置的精选连接(见 ./featured.ts),不参与 registry 搜索。 +export type McpRegistrySource = "official" | "smithery" | "glama" | "featured"; export type McpRegistryConfigInput = { name: string; @@ -943,3 +944,5 @@ export function applyMcpRegistryInstallConfig( commandPreview: commandPreview(server), }; } + +export { getFeaturedMcpRegistryCards } from "./featured"; diff --git a/crates/agent-gui/src/pages/mcp-hub/McpRegistryBrowser.tsx b/crates/agent-gui/src/pages/mcp-hub/McpRegistryBrowser.tsx index 3b9423dee..c2f00facc 100644 --- a/crates/agent-gui/src/pages/mcp-hub/McpRegistryBrowser.tsx +++ b/crates/agent-gui/src/pages/mcp-hub/McpRegistryBrowser.tsx @@ -32,6 +32,7 @@ import { useLocale } from "../../i18n"; import { applyMcpRegistryInstallConfig, createUniqueMcpServerId, + getFeaturedMcpRegistryCards, MCP_REGISTRY_SOURCE_OPTIONS, type McpRegistryCard, type McpRegistryConfigInput, @@ -1296,6 +1297,9 @@ export function McpRegistryBrowser(props: McpRegistryBrowserProps) { const [previewError, setPreviewError] = useState(null); const [installedByCardId, setInstalledByCardId] = useState>({}); const groupedItems = useMemo(() => groupMcpRegistryCards(items), [items]); + // 本地内置的精选连接(如 Obsidian),仅在未搜索时展示,避免挤占搜索结果。 + const featuredGroups = useMemo(() => groupMcpRegistryCards(getFeaturedMcpRegistryCards(t)), [t]); + const showFeatured = query.trim() === ""; const existingIds = useMemo( () => new Set(settings.mcp.servers.map((server) => server.id)), @@ -1508,6 +1512,26 @@ export function McpRegistryBrowser(props: McpRegistryBrowserProps) {
+ {showFeatured && featuredGroups.length > 0 ? ( +
+
+ + {t("mcpHub.featuredTitle")} +
+
+ {featuredGroups.map((group) => ( + void installCard(next)} + /> + ))} +
+
+ ) : null} {loading && items.length === 0 ? ( <>
diff --git a/crates/agent-gui/test/tools/mcp-registry.test.mjs b/crates/agent-gui/test/tools/mcp-registry.test.mjs index eb97c3986..2d274e296 100644 --- a/crates/agent-gui/test/tools/mcp-registry.test.mjs +++ b/crates/agent-gui/test/tools/mcp-registry.test.mjs @@ -417,3 +417,46 @@ test("server ids are made unique before adding a registry draft", () => { ]); assert.equal(unique.server.id, "demo-3"); }); + +test("featured obsidian connector ships a configurable stdio draft", () => { + const cards = registry.getFeaturedMcpRegistryCards((key) => key); + const obsidian = cards.find((card) => card.sourceId === "obsidian"); + assert.ok(obsidian, "obsidian featured card exists"); + assert.equal(obsidian.source, "featured"); + assert.equal(obsidian.displayName, "Obsidian"); + assert.equal(obsidian.description, "mcpHub.featuredObsidianDesc"); + + const draft = obsidian.installDraft; + assert.equal(draft.status, "needs_config"); + assert.equal(draft.server.enabled, false); + assert.equal(draft.server.transport, "stdio"); + assert.equal(draft.server.command, "uvx"); + assert.deepEqual(draft.server.args, ["mcp-obsidian"]); + assert.equal(draft.server.env.OBSIDIAN_HOST, "127.0.0.1"); + assert.equal(draft.server.env.OBSIDIAN_PORT, "27124"); + assert.equal(draft.server.env.OBSIDIAN_PROTOCOL, "https"); + assert.equal(draft.commandPreview, "uvx mcp-obsidian"); + + assert.deepEqual( + draft.requiredConfig.filter((input) => input.required).map((input) => input.name), + ["OBSIDIAN_API_KEY"], + ); + const apiKeyInput = draft.requiredConfig.find((input) => input.name === "OBSIDIAN_API_KEY"); + assert.equal(apiKeyInput.secret, true); + assert.equal(apiKeyInput.target, "env"); +}); + +test("applying the featured obsidian config yields an enabled server", () => { + const [obsidian] = registry.getFeaturedMcpRegistryCards((key) => key); + const configured = registry.applyMcpRegistryInstallConfig(obsidian.installDraft, { + "env:OBSIDIAN_API_KEY": "secret-key", + "env:OBSIDIAN_PORT": "27125", + }); + + assert.equal(configured.status, "ready"); + assert.equal(configured.server.enabled, true); + assert.equal(configured.server.env.OBSIDIAN_API_KEY, "secret-key"); + assert.equal(configured.server.env.OBSIDIAN_PORT, "27125"); + assert.equal(configured.server.env.OBSIDIAN_HOST, "127.0.0.1"); + assert.equal(configured.requiredConfig.length, 0); +}); diff --git a/docs/features/skills-and-mcp.md b/docs/features/skills-and-mcp.md index f81975aae..7bc734f5c 100644 --- a/docs/features/skills-and-mcp.md +++ b/docs/features/skills-and-mcp.md @@ -85,6 +85,7 @@ | official registry | 从 `registry.modelcontextprotocol.io` 读取官方 server 列表与 package metadata。 | | Smithery | 搜索 Smithery server,并尝试解析 install draft 或 manual draft。 | | Glama | 搜索 Glama MCP server 列表。 | +| featured(精选连接) | `lib/mcpRegistry/featured.ts` 本地内置的精选安装草稿(如 Obsidian · Local REST API),不发起网络请求,在 Store 未搜索时置顶展示,复用与 Store 相同的 needs_config 配置弹窗安装链路。 | Registry card 会被归一化为统一的 `McpRegistryCard`,其中 `installDraft` 表示可直接生成 server config,`manualDraft` 表示需要用户手工补全。 diff --git a/scripts/mirror-manifest.json b/scripts/mirror-manifest.json index 75083be2c..261e03fee 100644 --- a/scripts/mirror-manifest.json +++ b/scripts/mirror-manifest.json @@ -86,6 +86,7 @@ "lib/skills/clawHub.ts", "lib/skills/index.ts", "lib/mcpRegistry/index.ts", + "lib/mcpRegistry/featured.ts", "lib/shared/id.ts", "pages/settings/SystemToolsSection.tsx", "pages/mcp-hub/McpHubPage.tsx",