Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions crates/agent-gateway/web/src/i18n/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1653,6 +1653,17 @@ export const translations: Record<Locale, Record<string, string>> = {
"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 扩展可调用的技能模块",
Expand Down Expand Up @@ -3570,6 +3581,18 @@ export const translations: Record<Locale, Record<string, string>> = {
"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",
Expand Down
88 changes: 88 additions & 0 deletions crates/agent-gateway/web/src/lib/mcpRegistry/featured.ts
Original file line number Diff line number Diff line change
@@ -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)];
}
5 changes: 4 additions & 1 deletion crates/agent-gateway/web/src/lib/mcpRegistry/index.ts
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -943,3 +944,5 @@ export function applyMcpRegistryInstallConfig(
commandPreview: commandPreview(server),
};
}

export { getFeaturedMcpRegistryCards } from "./featured";
24 changes: 24 additions & 0 deletions crates/agent-gateway/web/src/pages/mcp-hub/McpRegistryBrowser.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ import { useLocale } from "../../i18n";
import {
applyMcpRegistryInstallConfig,
createUniqueMcpServerId,
getFeaturedMcpRegistryCards,
MCP_REGISTRY_SOURCE_OPTIONS,
type McpRegistryCard,
type McpRegistryConfigInput,
Expand Down Expand Up @@ -1292,6 +1293,9 @@ export function McpRegistryBrowser(props: McpRegistryBrowserProps) {
const [previewError, setPreviewError] = useState<string | null>(null);
const [installedByCardId, setInstalledByCardId] = useState<Record<string, string>>({});
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)),
Expand Down Expand Up @@ -1504,6 +1508,26 @@ export function McpRegistryBrowser(props: McpRegistryBrowserProps) {

<div className="min-h-0 flex-1 overflow-y-auto px-1 pb-4 pt-2">
<div className="flex flex-col gap-4">
{showFeatured && featuredGroups.length > 0 ? (
<div className="hub-panel-enter flex flex-col gap-2.5">
<div className="flex items-center gap-1.5 text-[11.5px] text-muted-foreground">
<Sparkles className="h-3.5 w-3.5 text-foreground/55" />
<span>{t("mcpHub.featuredTitle")}</span>
</div>
<div className="grid gap-3 sm:grid-cols-2 xl:grid-cols-3">
{featuredGroups.map((group) => (
<RegistryCard
key={group.id}
group={group}
installedIdForCard={installedIdForCard}
installingId={installingId}
onPreview={setPreviewCard}
onInstall={(next) => void installCard(next)}
/>
))}
</div>
</div>
) : null}
{loading && items.length === 0 ? (
<>
<div key={source} className="hub-frost-hero hub-panel-enter px-4 py-3.5">
Expand Down
23 changes: 23 additions & 0 deletions crates/agent-gui/src/i18n/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1716,6 +1716,17 @@ export const translations: Record<Locale, Record<string, string>> = {
"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 扩展可调用的技能模块",
Expand Down Expand Up @@ -3722,6 +3733,18 @@ export const translations: Record<Locale, Record<string, string>> = {
"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",
Expand Down
88 changes: 88 additions & 0 deletions crates/agent-gui/src/lib/mcpRegistry/featured.ts
Original file line number Diff line number Diff line change
@@ -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)];
}
5 changes: 4 additions & 1 deletion crates/agent-gui/src/lib/mcpRegistry/index.ts
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -943,3 +944,5 @@ export function applyMcpRegistryInstallConfig(
commandPreview: commandPreview(server),
};
}

export { getFeaturedMcpRegistryCards } from "./featured";
24 changes: 24 additions & 0 deletions crates/agent-gui/src/pages/mcp-hub/McpRegistryBrowser.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ import { useLocale } from "../../i18n";
import {
applyMcpRegistryInstallConfig,
createUniqueMcpServerId,
getFeaturedMcpRegistryCards,
MCP_REGISTRY_SOURCE_OPTIONS,
type McpRegistryCard,
type McpRegistryConfigInput,
Expand Down Expand Up @@ -1296,6 +1297,9 @@ export function McpRegistryBrowser(props: McpRegistryBrowserProps) {
const [previewError, setPreviewError] = useState<string | null>(null);
const [installedByCardId, setInstalledByCardId] = useState<Record<string, string>>({});
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)),
Expand Down Expand Up @@ -1508,6 +1512,26 @@ export function McpRegistryBrowser(props: McpRegistryBrowserProps) {

<div className="min-h-0 flex-1 overflow-y-auto px-1 pb-4 pt-2">
<div className="flex flex-col gap-4">
{showFeatured && featuredGroups.length > 0 ? (
<div className="hub-panel-enter flex flex-col gap-2.5">
<div className="flex items-center gap-1.5 text-[11.5px] text-muted-foreground">
<Sparkles className="h-3.5 w-3.5 text-foreground/55" />
<span>{t("mcpHub.featuredTitle")}</span>
</div>
<div className="grid gap-3 sm:grid-cols-2 xl:grid-cols-3">
{featuredGroups.map((group) => (
<RegistryCard
key={group.id}
group={group}
installedIdForCard={installedIdForCard}
installingId={installingId}
onPreview={setPreviewCard}
onInstall={(next) => void installCard(next)}
/>
))}
</div>
</div>
) : null}
{loading && items.length === 0 ? (
<>
<div key={source} className="hub-frost-hero hub-panel-enter px-4 py-3.5">
Expand Down
Loading
Loading