From cbc4631315ab15e7bb612791b15f03ebbddbf98b Mon Sep 17 00:00:00 2001 From: Darkatse <39111369+Darkatse@users.noreply.github.com> Date: Mon, 10 Aug 2026 01:11:58 -0400 Subject: [PATCH] feat(mcp): add removable Exa Search recommendation --- docs/API/MCP.md | 1 + .../mcp-manager/src/McpManagerApp.test.tsx | 30 ++++++++++++++++ .../mcp-manager/src/McpManagerApp.tsx | 5 +-- .../extensions/mcp-manager/src/index.tsx | 15 ++++++-- .../mcp-manager/src/recommendation.ts | 35 +++++++++++++++++++ 5 files changed, 82 insertions(+), 4 deletions(-) create mode 100644 src/scripts/extensions/mcp-manager/src/recommendation.ts diff --git a/docs/API/MCP.md b/docs/API/MCP.md index a7b06b85..20c7bd27 100644 --- a/docs/API/MCP.md +++ b/docs/API/MCP.md @@ -103,6 +103,7 @@ type McpTestCallOutcome = ## 4. Registration 契约 - `create()` 总是创建 `paused` registration;Manager 在切换为 Active 前展示并确认 exact endpoint。 +- Manager 首次加载时会通过现有 `create()` 契约添加一次 Paused 的 Exa Search 推荐项;处理标记保存在扩展 store 中。删除该普通 registration 不会清除标记,因此同一 data root 中不会自动恢复。 - endpoint 是 registration 的信任身份事实,当前不提供修改方法。更换 endpoint 必须新建 UUID,工具权限重新从 Off 开始。 - display name 可以修改,不影响 UUID 或 ToolId。 - `off` 是缺省值,不写入 `toolPermissions`;`setPermission(..., 'off')` 删除对应持久设置。 diff --git a/src/scripts/extensions/mcp-manager/src/McpManagerApp.test.tsx b/src/scripts/extensions/mcp-manager/src/McpManagerApp.test.tsx index 1ebfca7a..20fb1ed1 100644 --- a/src/scripts/extensions/mcp-manager/src/McpManagerApp.test.tsx +++ b/src/scripts/extensions/mcp-manager/src/McpManagerApp.test.tsx @@ -9,6 +9,7 @@ import { } from './McpManagerApp'; import { openAddServerDialog, tr } from './host'; import { installPopupHost, TestPopup, uninstallPopupHost } from './popup-stub'; +import { ensureExaRecommendation } from './recommendation'; const SERVER_ID = '11111111-1111-4111-8111-111111111111'; @@ -87,6 +88,35 @@ test('adds a server through the dialog action and lists it paused', async () => expect(screen.getByRole('button', { name: 'Paused' })).toBeTruthy(); }); +test('creates the Exa recommendation once and does not restore it after deletion', async () => { + let handled = false; + const creates: Array<{ displayName: string; endpoint: string }> = []; + const store = { + tryGetJson: () => Promise.resolve({ found: handled }), + setJson: () => { + handled = true; + return Promise.resolve(); + }, + }; + const create = (input: { displayName: string; endpoint: string }) => { + creates.push(input); + return Promise.resolve({ ...server(), ...input }); + }; + + const first = await ensureExaRecommendation(initial(), create, store); + expect(first.error).toBeUndefined(); + expect(first.initial.servers).toEqual([{ ...server(), ...creates[0] }]); + expect(creates).toEqual([{ + displayName: 'Exa Search', + endpoint: 'https://mcp.exa.ai/mcp', + }]); + + const afterDeletion = await ensureExaRecommendation(initial(), create, store); + expect(afterDeletion.error).toBeUndefined(); + expect(afterDeletion.initial.servers).toEqual([]); + expect(creates).toHaveLength(1); +}); + test('discovers tools on expand and persists an explicit permission choice', async () => { const permissionCalls: Array<{ registrationId: string; diff --git a/src/scripts/extensions/mcp-manager/src/McpManagerApp.tsx b/src/scripts/extensions/mcp-manager/src/McpManagerApp.tsx index fd589c63..8b4c1aa0 100644 --- a/src/scripts/extensions/mcp-manager/src/McpManagerApp.tsx +++ b/src/scripts/extensions/mcp-manager/src/McpManagerApp.tsx @@ -23,6 +23,7 @@ export type McpManagerActions = { type McpManagerAppProps = { initial: McpManagerInitial; + initialError?: string; actions: McpManagerActions; tr: McpTranslator; }; @@ -32,14 +33,14 @@ type ServerStatus = { error: string; }; -export function McpManagerApp({ initial, actions, tr }: McpManagerAppProps) { +export function McpManagerApp({ initial, initialError = '', actions, tr }: McpManagerAppProps) { const [servers, setServers] = useState(initial.servers); const [discoveries, setDiscoveries] = useState>({}); const [expanded, setExpanded] = useState>({}); const [statuses, setStatuses] = useState>({}); const [adding, setAdding] = useState(false); const [testing, setTesting] = useState(false); - const [panelError, setPanelError] = useState(''); + const [panelError, setPanelError] = useState(initialError); const sortedServers = [...servers].sort((left, right) => ( left.displayName.localeCompare(right.displayName) || left.id.localeCompare(right.id) diff --git a/src/scripts/extensions/mcp-manager/src/index.tsx b/src/scripts/extensions/mcp-manager/src/index.tsx index c43c3c15..2821f0b6 100644 --- a/src/scripts/extensions/mcp-manager/src/index.tsx +++ b/src/scripts/extensions/mcp-manager/src/index.tsx @@ -4,6 +4,7 @@ import { createRoot } from 'react-dom/client'; import { confirmActivate, confirmRemove, + errorText, openAddServerDialog, promptServerName, requireMcpApi, @@ -11,6 +12,7 @@ import { waitForHostReady, } from './host'; import { McpManagerApp, type McpManagerActions } from './McpManagerApp'; +import { ensureExaRecommendation } from './recommendation'; import { openTestCallDialog } from './test-call-dialog'; const CONTAINER_ID = 'mcp_manager_container'; @@ -43,7 +45,11 @@ function ensureContainer(): HTMLElement { async function mountMcpManager(): Promise { await waitForHostReady(); const api = requireMcpApi(); - const initial = await api.servers.list(); + const listed = await api.servers.list(); + const store = window.__TAURITAVERN__?.api?.extension?.store; + const recommendation = store + ? await ensureExaRecommendation(listed, api.servers.create, store) + : { initial: listed, error: new Error('TauriTavern extension store is unavailable') }; const actions: McpManagerActions = { addServer: () => openAddServerDialog(api.servers.create), renameServer: async (server) => { @@ -70,7 +76,12 @@ async function mountMcpManager(): Promise { createRoot(ensureContainer()).render( - + , ); } diff --git a/src/scripts/extensions/mcp-manager/src/recommendation.ts b/src/scripts/extensions/mcp-manager/src/recommendation.ts new file mode 100644 index 00000000..a50e9474 --- /dev/null +++ b/src/scripts/extensions/mcp-manager/src/recommendation.ts @@ -0,0 +1,35 @@ +const EXA_SEARCH = { + displayName: 'Exa Search', + endpoint: 'https://mcp.exa.ai/mcp', +}; + +const HANDLED_MARKER = { + namespace: 'tauritavern.mcp-manager', + key: 'exa-search-recommendation-handled', +}; + +type McpServerList = Awaited>; +type RecommendationStore = Pick; + +export async function ensureExaRecommendation( + initial: McpServerList, + create: TauriTavernMcpApi['servers']['create'], + store: RecommendationStore, +): Promise<{ initial: McpServerList; error?: unknown }> { + let next = initial; + try { + if ((await store.tryGetJson(HANDLED_MARKER)).found) { + return { initial }; + } + + if (!initial.servers.some(server => server.endpoint === EXA_SEARCH.endpoint)) { + const server = await create(EXA_SEARCH); + next = { ...initial, servers: [...initial.servers, server] }; + } + + await store.setJson({ ...HANDLED_MARKER, value: true }); + return { initial: next }; + } catch (error) { + return { initial: next, error }; + } +}