dsh-search-mcp/lib/client.browser.js
oliver d5c4d2006c Publish dsh-search-mcp fork for newer DSH and Bailian WebSearch MCP.
Based on gxpppp/dsh-search-mcp; includes DSH web settings fixes, Clash fake-IP URL policy, and Bailian default server patch.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-12 08:51:32 +08:00

1077 lines
49 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/*
* dsh-search-mcp — browser half.
*
* Registers one card into the Settings → Plugins → 插件配置 (configurable) tab,
* bound to the `search-mcp` settings namespace registered by the host half.
*
* The card is a structured form (no raw JSON editing), designed to feel like
* the neighbor cards:
* - 默认服务器: dropdown of the configured server ids (friendly labels)
* - 结果数上限 / 搜索超时: numeric inputs (timeout in seconds)
* - 常用提供商 quick-add: one-click preset rows (Tavily / Brave / Exa /
* Perplexity / DuckDuckGo) — only the API key still needs filling
* - 服务器列表: collapsible rows; known providers show only identity,
* provider, write-only key/reference status, and result limit; custom rows
* additionally expose transport, URL/command, authentication, and tool name.
*
* Loaded through window.__ModuleLoader__ like every shipped client bundle.
*/
window.__ModuleLoader__.load({
id: "dsh-search-mcp",
factory: (require) => {
var module = { exports: {} };
var exports = module.exports;
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
let react_jsx_runtime = require("react/jsx-runtime");
let react = require("react");
let _deepseek_ai_dsh_client_ui_primitives = require("@deepseek-ai/dsh-client-ui-primitives");
// 0.1.2+: createSnapshotStore lives on the platform seed @deepseek-ai/dsh-client-store
// (old @deepseek-ai/dsh-client-runtime/client is gone from the module table).
let _deepseek_ai_dsh_client_runtime_client = require("@deepseek-ai/dsh-client-store");
//#region styles
const css = [
".smcp_card{display:flex;flex-direction:column;border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-3);border-radius:12px;list-style:none;transition:border-color .16s,background .16s}",
".smcp_card:hover{border-color:var(--dsw-alias-label-dimmed)}",
".smcp_cardOpen{background:var(--dsw-alias-bg-layer-2);border-color:var(--dsw-alias-label-dimmed)}",
".smcp_headBtn{appearance:none;width:100%;font:inherit;color:inherit;text-align:left;cursor:pointer;background:0 0;border:0;border-radius:12px;align-items:center;gap:12px;padding:14px 16px;display:flex}",
".smcp_headBtn:focus-visible{outline:2px solid var(--dsw-alias-brand-primary);outline-offset:-2px}",
".smcp_head{flex-direction:column;flex:1;gap:4px;min-width:0;display:flex}",
".smcp_name{color:var(--dsw-alias-label-primary);font-size:15px;font-weight:600;line-height:1.4}",
".smcp_desc{color:var(--dsw-alias-label-tertiary);font-size:13px;line-height:1.5}",
".smcp_chevron{color:var(--dsw-alias-label-tertiary);flex:none;transition:transform .16s}",
".smcp_chevronOpen{transform:rotate(180deg)}",
".smcp_body{border-top:1px solid var(--dsw-alias-border-l2);margin:0 16px;padding-bottom:8px;display:flex;flex-direction:column;gap:0}",
".smcp_field{display:flex;flex-direction:column;gap:6px;padding:12px 0}",
".smcp_field + .smcp_field{border-top:1px solid var(--dsw-alias-border-l2)}",
".smcp_label{display:flex;align-items:center;gap:8px;color:var(--dsw-alias-label-primary);font-size:13px;font-weight:500;line-height:1.5}",
".smcp_hint{color:var(--dsw-alias-label-tertiary);margin:0;font-size:12px;line-height:1.5}",
".smcp_input{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-3);height:34px;font:inherit;color:var(--dsw-alias-label-primary);border-radius:8px;padding:0 12px;font-size:13px;line-height:1.5}",
".smcp_input:focus-visible{border-color:var(--dsw-alias-brand-primary);outline:none}",
".smcp_select{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-3);height:34px;font:inherit;color:var(--dsw-alias-label-primary);border-radius:8px;padding:0 8px;font-size:13px;line-height:1.5}",
".smcp_select:focus-visible{border-color:var(--dsw-alias-brand-primary);outline:none}",
".smcp_invalid{border-color:var(--dsw-alias-label-error)}",
".smcp_error{color:var(--dsw-alias-label-error);margin:0;font-size:12px;line-height:1.5}",
".smcp_badge{white-space:nowrap;background:var(--dsw-alias-bg-module-platform);color:var(--dsw-alias-label-secondary);border-radius:999px;padding:1px 8px;font-size:11px;font-weight:500;line-height:17px}",
".smcp_badgeMuted{white-space:nowrap;color:var(--dsw-alias-label-tertiary);border-radius:999px;padding:1px 8px;font-size:11px;line-height:17px}",
".smcp_badgeDanger{white-space:nowrap;color:var(--dsw-alias-label-error);border:1px solid var(--dsw-alias-label-error);border-radius:999px;padding:1px 8px;font-size:11px;line-height:17px}",
".smcp_reset{font:inherit;color:var(--dsw-alias-label-secondary);cursor:pointer;background:0 0;border:none;padding:0;font-size:12px;line-height:1.5}",
".smcp_reset:hover:not(:disabled){color:var(--dsw-alias-label-primary)}",
".smcp_reset:disabled{cursor:default}",
".smcp_footer{display:flex;justify-content:flex-end;align-items:center;gap:8px;padding-top:12px;border-top:1px solid var(--dsw-alias-border-l2)}",
".smcp_btn{appearance:none;font:inherit;cursor:pointer;border:1px solid transparent;border-radius:8px;padding:5px 12px;height:auto;min-height:32px;font-size:13px;line-height:1.5}",
/* Match system plugin cards (e.g. dsh-ops-cron): primary text on contrast surface */
".smcp_btnPrimary{background:var(--dsw-alias-label-primary);color:var(--dsw-alias-bg-layer-3);border-color:transparent}",
".smcp_btnPrimary:disabled{opacity:.5;cursor:default}",
".smcp_btnGhost{border-color:var(--dsw-alias-border-l2);background:transparent;color:var(--dsw-alias-label-secondary)}",
".smcp_btnGhost:disabled{opacity:.5;cursor:default}",
".smcp_failDetail{color:var(--dsw-alias-label-error);flex:1;margin:0;font-size:12px;line-height:1.45;text-align:left}",
".smcp_quickAdd{display:flex;flex-wrap:wrap;gap:8px;padding:2px 0 6px}",
".smcp_quickBtn{border:1px solid var(--dsw-alias-border-l2);background:0 0;color:var(--dsw-alias-label-secondary);border-radius:999px;padding:4px 12px;font-size:12px;line-height:1.5;cursor:pointer}",
".smcp_quickBtn:hover:not(:disabled){color:var(--dsw-alias-label-primary);border-color:var(--dsw-alias-border-l3)}",
".smcp_quickBtn:disabled{opacity:.5;cursor:default}",
".smcp_row{border:1px solid var(--dsw-alias-border-l2);border-radius:10px;padding:10px 12px;display:flex;flex-direction:column;gap:8px;background:var(--dsw-alias-bg-layer-2)}",
".smcp_rowHead{display:flex;align-items:center;gap:8px;min-width:0}",
".smcp_rowToggle{appearance:none;border:0;background:0 0;padding:2px;cursor:pointer;color:var(--dsw-alias-label-tertiary);display:inline-flex;align-items:center;flex:none;transition:transform .16s}",
".smcp_rowToggle:focus-visible{outline:2px solid var(--dsw-alias-brand-primary);outline-offset:-2px;border-radius:6px}",
".smcp_rowToggleOpen{transform:rotate(180deg)}",
".smcp_kindBadge{white-space:nowrap;background:var(--dsw-alias-bg-module-platform);color:var(--dsw-alias-label-secondary);border-radius:999px;padding:1px 8px;font-size:11px;font-weight:500;line-height:17px;flex:none}",
".smcp_rowTitle{flex:0 0 auto;color:var(--dsw-alias-label-primary);font-size:13px;font-weight:600;line-height:1.5}",
".smcp_rowSummary{flex:1;min-width:0;color:var(--dsw-alias-label-tertiary);font-size:12px;line-height:1.5;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}",
".smcp_rowBody{border-top:1px dashed var(--dsw-alias-border-l2);margin-top:2px;padding-top:8px;display:flex;flex-direction:column;gap:8px}",
".smcp_rowId{flex:1;min-width:0}",
".smcp_rowGrid{display:grid;grid-template-columns:1fr 1fr;gap:8px}",
".smcp_rowGrid3{display:grid;grid-template-columns:1fr 1fr 1fr;gap:8px}",
".smcp_cell{display:flex;flex-direction:column;gap:4px;min-width:0}",
".smcp_cellLabel{color:var(--dsw-alias-label-secondary);font-size:11px;line-height:1.5}",
".smcp_cellHint{color:var(--dsw-alias-label-tertiary);margin:0;font-size:11px;line-height:1.5}",
".smcp_add{display:inline-flex;align-items:center;gap:6px;align-self:flex-start;border:1px dashed var(--dsw-alias-border-l2);background:0 0;color:var(--dsw-alias-label-secondary);border-radius:8px;padding:6px 12px;font-size:12px;line-height:1.5;cursor:pointer}",
".smcp_add:hover{color:var(--dsw-alias-label-primary);border-color:var(--dsw-alias-border-l3)}",
".smcp_del{border:1px solid var(--dsw-alias-border-l2);background:0 0;color:var(--dsw-alias-label-secondary);border-radius:8px;padding:4px 10px;font-size:12px;line-height:1.5;cursor:pointer;flex:none}",
".smcp_del:hover{color:var(--dsw-alias-label-error);border-color:var(--dsw-alias-label-error)}"
].join("");
const cssTag = "dsh-search-mcp/card.css";
if (typeof document !== "undefined" && document.querySelector(`style[data-plugin-css="${cssTag}"]`) === null) {
const tag = document.createElement("style");
tag.dataset.plugin = "dsh-search-mcp";
tag.dataset.pluginCss = cssTag;
tag.textContent = css;
document.head.appendChild(tag);
}
//#endregion
//#region provider catalog (browser-side metadata only)
const CATALOG = {
tavily: { needsKey: true },
brave: { needsKey: true },
exa: { needsKey: true },
perplexity: { needsKey: true },
duckduckgo: { needsKey: false },
custom: { needsKey: false }
};
const KIND_OPTIONS = Object.keys(CATALOG);
const TRANSPORT_OPTIONS = ["http", "stdio"];
const AUTH_STYLE_OPTIONS = ["", "query", "header"];
/** Brand labels shown on the kind badge / quick-add buttons. */
const KIND_LABELS = {
tavily: "Tavily",
brave: "Brave",
exa: "Exa",
perplexity: "Perplexity",
duckduckgo: "DuckDuckGo",
custom: "Custom"
};
/** Kinds offered as one-click presets (custom is added via the dashed button). */
const QUICK_KINDS = ["tavily", "brave", "exa", "perplexity", "duckduckgo"];
//#endregion
//#region locale
const en = {
title: "Search MCP",
description: "Search MCP servers behind the web_search tool; the built-in DeepSeek search stays disabled while this plugin is enabled.",
defaultServer: "Default server",
defaultServerHint: "Which server row serves searches (empty = first row).",
maxResults: "Max results",
maxResultsHint: "Sources returned per search (1–50; empty = default).",
searchTimeoutMs: "Search timeout (s)",
searchTimeoutMsHint: "Abort a search that takes longer than this.",
servers: "Search MCP servers",
serversHint: "Choose a provider and enter its CDKey/API key. Known providers keep connection details in the host catalog; custom rows expose advanced MCP settings.",
quickAdd: "Common providers",
quickAddHint: "Add a provider row, then enter its CDKey/API key before saving. No endpoint is required for known providers.",
addServer: "Add custom server",
removeServer: "Delete",
rowId: "ID",
rowIdHint: "Stable id used by “default server”.",
rowKind: "Provider",
rowTransport: "Transport",
rowUrl: "Endpoint (URL)",
rowCommand: "Command",
rowArgs: "Args (comma separated)",
rowApiKey: "API key",
cdKey: "CDKey / API key",
cdKeyHint: "enter CDKey / API key",
keyNotRequired: "no key required",
rowApiKeyEnv: "Key env/credential ref",
rowAuthStyle: "Key placement",
rowAuthParam: "Key param / env name",
rowToolName: "MCP tool name",
rowMaxResults: "Max results",
rowMaxResultsHint: "Empty = follow the global max results.",
missingId: "ID is required.",
dupId: "This ID is already used by another row.",
overridden: "Overridden",
overridesGlobal: "overrides global",
keySet: "key set",
keyRef: "credential ref",
keyConfigured: "credential configured",
keyRefMissing: "credential missing",
keyMissing: "key missing",
legacyKey: "legacy key",
legacyKeyBlocked: "This server still uses a hidden legacy literal key. Enter a replacement key or credential reference before changing the server list.",
customKind: "Custom",
reset: "Reset to default",
readOnly: "This deployment stores settings read-only.",
save: "Save",
saving: "Saving…",
discard: "Discard",
unsaved: "Unsaved",
saveFailed: "The deployment did not accept these values; they were left for you to correct.",
placeholderUrl: "https://mcp.example.com/mcp/",
placeholderCommand: "npx",
placeholderArgs: "-y, duckduckgo-mcp-server",
placeholderToolName: "tavily_search",
placeholderAuthParam: "tavilyApiKey"
};
const zh = {
title: "搜索 MCP",
description: "web_search 工具背后的搜索 MCP 服务器;插件启用期间内置 DeepSeek 搜索保持禁用。",
defaultServer: "默认服务器",
defaultServerHint: "使用哪一行服务器(留空 = 第一行)。",
maxResults: "结果数上限",
maxResultsHint: "每次搜索最多返回的条数(1–50;留空 = 默认)。",
searchTimeoutMs: "搜索超时(秒)",
searchTimeoutMsHint: "超过该时长即中止搜索。",
servers: "搜索 MCP 服务器",
serversHint: "选择提供商并填写 CDKey/API key。已知提供商的连接细节由 Host catalog 管理;自定义行才显示 MCP 高级设置。",
quickAdd: "常用提供商",
quickAddHint: "添加提供商行后填写 CDKey/API key 再保存。已知提供商不需要填写端点链接。",
addServer: "添加自定义服务器",
removeServer: "删除",
rowId: "ID",
rowIdHint: "“默认服务器”下拉框引用的稳定标识。",
rowKind: "提供商",
rowTransport: "传输方式",
rowUrl: "端点(URL)",
rowCommand: "命令",
rowArgs: "参数(逗号分隔)",
rowApiKey: "API 密钥",
cdKey: "CDKey / API key",
cdKeyHint: "请填写 CDKey / API key",
keyNotRequired: "无需密钥",
rowApiKeyEnv: "密钥环境变量/凭证引用",
rowAuthStyle: "密钥位置",
rowAuthParam: "密钥参数名/环境变量名",
rowToolName: "MCP 工具名",
rowMaxResults: "结果数上限",
rowMaxResultsHint: "留空 = 跟随全局结果数上限。",
missingId: "ID 不能为空。",
dupId: "该 ID 已被另一行占用。",
overridden: "已覆盖",
overridesGlobal: "覆盖全局",
keySet: "已填密钥",
keyRef: "凭证引用",
keyConfigured: "凭证已配置",
keyRefMissing: "凭证未配置",
keyMissing: "缺少密钥",
legacyKey: "旧版密钥",
legacyKeyBlocked: "此服务器仍使用客户端不可见的旧版字面密钥。修改服务器列表前,请输入替代密钥或凭证引用。",
customKind: "自定义",
reset: "恢复默认",
readOnly: "本部署的设置为只读。",
save: "保存",
saving: "保存中…",
discard: "放弃修改",
unsaved: "未保存",
saveFailed: "本部署没有接受这些值,已保留供你修改。",
placeholderUrl: "https://mcp.example.com/mcp/",
placeholderCommand: "npx",
placeholderArgs: "-y, duckduckgo-mcp-server",
placeholderToolName: "tavily_search",
placeholderAuthParam: "tavilyApiKey"
};
//#endregion
/** Namespace of this plugin's settings section (spelled, not imported). */
const NS = "search-mcp";
//#region row helpers
function emptyRow() {
return {
id: "",
kind: "custom",
transport: "http",
url: "",
command: "",
args: "",
apiKey: "",
apiKeyEnv: "",
authStyle: "",
authParam: "",
authPrefix: "",
toolName: "",
maxResults: ""
};
}
/** Convert a stored server entry (host shape) to a row draft. */
function rowFromEntry(entry) {
const kind = Object.hasOwn(CATALOG, entry.kind) ? entry.kind : "custom";
const known = kind !== "custom";
return {
id: entry.id ?? "",
kind,
transport: known ? "" : (entry.transport ?? "http"),
url: known ? "" : (entry.url ?? ""),
command: known ? "" : (entry.command ?? ""),
args: known ? "" : (Array.isArray(entry.args) ? entry.args.join(", ") : ""),
apiKey: entry.apiKey ?? "",
apiKeyEnv: entry.apiKeyEnv ?? "",
authStyle: known ? "" : (entry.authStyle ?? ""),
authParam: known ? "" : (entry.authParam ?? ""),
authPrefix: known ? "" : (entry.authPrefix ?? ""),
toolName: known ? "" : (entry.toolName ?? ""),
maxResults: entry.maxResults === undefined ? "" : String(entry.maxResults)
};
}
/** Convert a row draft to the minimal known entry or full custom entry. */
function entryFromRow(row) {
const entry = { id: row.id.trim(), kind: row.kind };
const known = Object.hasOwn(CATALOG, row.kind) && row.kind !== "custom";
if (!known) {
entry.transport = row.transport || "http";
if (entry.transport === "stdio") {
if (row.command.trim() !== "") entry.command = row.command.trim();
const args = row.args.split(",").map((a) => a.trim()).filter(Boolean);
if (args.length > 0) entry.args = args;
} else {
const url = normalizeServerUrl(row);
if (url !== "") entry.url = url;
}
if (row.authStyle !== "") entry.authStyle = row.authStyle;
if (row.authParam.trim() !== "") entry.authParam = row.authParam.trim();
const prefix = resolveAuthPrefix(row);
if (prefix !== "") entry.authPrefix = prefix;
if (row.toolName.trim() !== "") entry.toolName = row.toolName.trim();
}
if (row.apiKey.trim() !== "") entry.apiKey = row.apiKey.trim();
if (row.apiKeyEnv.trim() !== "") entry.apiKeyEnv = row.apiKeyEnv.trim();
if (row.maxResults.trim() !== "") {
const n = Number(row.maxResults);
if (Number.isFinite(n) && n > 0) entry.maxResults = Math.round(n);
}
return entry;
}
/** Changing provider starts a clean credential/connection draft. */
function applyKindDefaults(row, kind) {
const next = { ...row, kind, apiKey: "", apiKeyEnv: "" };
if (kind !== "custom") {
return { ...next, transport: "", url: "", command: "", args: "", authStyle: "", authParam: "", toolName: "" };
}
return { ...next, transport: row.transport || "http" };
}
/** Suggest a unique row id for a preset kind (tavily, tavily2, …). */
function suggestId(servers, kind) {
const used = new Set(servers.map((row) => row.id.trim()).filter(Boolean));
let id = kind;
let n = 2;
while (used.has(id)) id = `${kind}${n++}`;
return id;
}
/** Brand label for a kind, localized for `custom`. */
function kindLabel(kind, t) {
if (kind === "custom") return t("customKind");
return KIND_LABELS[kind] ?? kind;
}
function deepEqualJson(left, right) {
return JSON.stringify(left) === JSON.stringify(right);
}
/** Snapshot-safe copy — `createSnapshotStore.set` deep-freezes state in
* non-production, so the live editable draft must never be published by reference. */
function cloneDraft(draft) {
return {
defaultServer: draft.defaultServer,
maxResults: draft.maxResults,
searchTimeoutMs: draft.searchTimeoutMs,
servers: draft.servers.map((row) => ({ ...row })),
};
}
function credentialRefFor(row) {
if (row.apiKeyEnv.trim() !== "") return row.apiKeyEnv.trim();
const url = (row.url || "").toLowerCase();
if (url.includes("dashscope.aliyuncs.com")) return "DASHSCOPE_API_KEY";
const id = row.id.trim().replace(/[^A-Za-z0-9]+/g, "_").replace(/^_+|_+$/g, "").toUpperCase();
return `SEARCH_MCP_${id || "SERVER"}_API_KEY`;
}
/** Normalize Bailian WebSearch endpoint if the user truncated it. */
function normalizeServerUrl(row) {
let url = (row.url || "").trim();
if (!url) return url;
if (row.toolName === "bailian_web_search" || url.includes("dashscope.aliyuncs.com")) {
if (url === "https://dashscope.aliyuncs.com/api/v1"
|| url === "https://dashscope.aliyuncs.com/api/v1/"
|| /\/api\/v1\/?$/.test(url) && url.includes("dashscope")) {
url = "https://dashscope.aliyuncs.com/api/v1/mcps/WebSearch/mcp";
}
if (url.endsWith("/WebSearch/sse")) {
url = url.replace(/\/WebSearch\/sse$/, "/WebSearch/mcp");
}
}
return url;
}
function resolveAuthPrefix(row) {
const explicit = (row.authPrefix || "").trim();
if (explicit !== "") return explicit.endsWith(" ") ? explicit : `${explicit} `;
if (row.authStyle === "header" && row.authParam.trim() === "Authorization") return "Bearer ";
return "";
}
const CREDENTIAL_DESCRIBE_BATCH_SIZE = 64;
/** 0.1.2+ Remote face: `remote.credentials.describe(refs)` → `{ ok, value }`. */
async function describeCredentialRefs(remote, refs) {
const states = {};
for (let index = 0; index < refs.length; index += CREDENTIAL_DESCRIBE_BATCH_SIZE) {
const batch = refs.slice(index, index + CREDENTIAL_DESCRIBE_BATCH_SIZE);
const response = await remote.credentials.describe(batch);
if (!response.ok) throw new Error(response.error?.message || "credential state lookup failed");
Object.assign(states, response.value ?? {});
}
return states;
}
async function rollbackNewCredentialRefs(remote, refs) {
for (const ref of [...new Set(refs)].reverse()) {
try {
await remote.credentials.unset(ref);
} catch {
// The save already failed; preserve the original error state.
}
}
}
async function rollbackSettingsWrites(scope, writes) {
for (const [field, previous] of writes.reverse()) {
try {
if (previous === undefined) await scope.unset(field);
else await scope.set(field, previous);
} catch {
// The UI remains failed so the user can retry or discard the draft.
}
}
}
//#endregion
//#region controller (whole-section staged draft)
var SearchMcpCardController = class {
constructor(scope, remote, describeFace) {
this.scope = scope;
this.remote = remote;
this.describeFace = describeFace;
this.draft = null;
this.listeners = new Set();
this.saving = false;
this.failed = false;
this.failDetail = "";
this.legacyBlocked = false;
this.secretStateReady = false;
this.legacySecretIds = new Set();
this.credentialStates = {};
this.store = (0, _deepseek_ai_dsh_client_runtime_client.createSnapshotStore)(this.project());
scope.subscribe(() => {
if (this.draft === null) this.publish();
this.readSecretState();
});
this.readSecretState();
}
section() {
return this.scope.getSnapshot();
}
value() {
return this.section().value ?? {};
}
userLayer() {
return this.section().user;
}
baseDraft() {
const value = this.value();
return {
defaultServer: typeof value.defaultServer === "string" ? value.defaultServer : "",
maxResults: typeof value.maxResults === "number" ? String(value.maxResults) : "",
searchTimeoutMs: typeof value.searchTimeoutMs === "number" ? String(value.searchTimeoutMs) : "",
servers: Array.isArray(value.servers) ? value.servers.map((entry) => ({
...rowFromEntry(entry),
legacySecret: this.legacySecretIds.has(entry.id)
})) : []
};
}
async readSecretState() {
let view;
try {
const response = await this.remote.settings.describe();
if (!response.ok) {
this.secretStateReady = true;
this.publish();
return;
}
view = (response.value?.namespaces ?? []).find((candidate) => candidate.ns === NS);
} catch {
this.secretStateReady = true;
this.publish();
return;
}
const servers = Array.isArray(view?.value?.servers) ? view.value.servers : [];
const ids = new Set();
for (const secret of view?.secrets ?? []) {
if (!secret.set || secret.path?.[0] !== "servers" || secret.path?.[2] !== "apiKey") continue;
const index = Number(secret.path[1]);
const id = Number.isInteger(index) ? servers[index]?.id : undefined;
if (typeof id === "string" && id.length > 0) ids.add(id);
}
const wasReady = this.secretStateReady;
const changed = !deepEqualJson([...ids].sort(), [...this.legacySecretIds].sort());
this.legacySecretIds = ids;
const previousCredentialStates = this.credentialStates;
const refs = [...new Set(servers.map((entry) => typeof entry.apiKeyEnv === "string" ? entry.apiKeyEnv.trim() : "").filter(Boolean))];
if (refs.length > 0) {
try {
this.credentialStates = await describeCredentialRefs(this.remote, refs);
} catch {
this.credentialStates = {};
}
} else {
this.credentialStates = {};
}
this.secretStateReady = true;
const credentialsChanged = !deepEqualJson(previousCredentialStates, this.credentialStates);
if (this.draft === null && (changed || credentialsChanged || !wasReady)) this.publish();
}
ensureDraft() {
if (this.draft === null) this.draft = this.baseDraft();
return this.draft;
}
project() {
const snapshot = this.section();
const draft = this.draft;
const sectionValue = snapshot.value ?? {};
const dirty = draft !== null && JSON.stringify(draft) !== JSON.stringify(this.baseDraft());
const ids = draft !== null ? draft.servers.map((row) => row.id.trim()) : [];
const invalid = draft !== null && (ids.some((id) => id === "") || new Set(ids.filter(Boolean)).size !== ids.filter(Boolean).length);
const serverKinds = draft !== null
? Object.fromEntries(draft.servers.map((row) => [row.id.trim(), row.kind]))
: Object.fromEntries((Array.isArray(sectionValue.servers) ? sectionValue.servers : []).map((s) => [s.id, s.kind]));
return {
available: snapshot.status === "ready",
// Do not gate edits on secretStateReady — a failed describe must not freeze the form.
writable: snapshot.writable === true,
dirty,
invalid,
saving: this.saving,
failed: this.failed,
failDetail: this.failDetail,
legacyBlocked: this.legacyBlocked,
overridden: this.userLayer() !== null && typeof this.userLayer() === "object" && !Array.isArray(this.userLayer())
? Object.keys(this.userLayer()).length > 0
: false,
value: draft !== null ? cloneDraft(draft) : this.baseDraft(),
serverIds: draft !== null
? ids.filter(Boolean)
: (Array.isArray(sectionValue.servers) ? sectionValue.servers.map((s) => s.id).filter(Boolean) : []),
serverKinds,
credentialStates: this.credentialStates
};
}
publish() {
this.store.set(this.project());
}
actions() {
return {
editScalar: (field, text) => {
this.ensureDraft()[field] = text;
this.failed = false;
this.legacyBlocked = false;
this.publish();
},
editRow: (index, field, value) => {
const draft = this.ensureDraft();
draft.servers[index] = { ...draft.servers[index], [field]: value };
this.failed = false;
this.legacyBlocked = false;
this.publish();
},
changeKind: (index, kind) => {
const draft = this.ensureDraft();
draft.servers[index] = applyKindDefaults(draft.servers[index], kind);
this.failed = false;
this.legacyBlocked = false;
this.publish();
},
addServer: () => {
const draft = this.ensureDraft();
draft.servers.push(emptyRow());
this.failed = false;
this.legacyBlocked = false;
this.publish();
},
quickAdd: (kind) => {
const draft = this.ensureDraft();
const id = suggestId(draft.servers, kind);
const row = applyKindDefaults({ ...emptyRow(), id, kind }, kind);
draft.servers.push(row);
this.failed = false;
this.legacyBlocked = false;
this.publish();
},
removeServer: (index) => {
const draft = this.ensureDraft();
draft.servers.splice(index, 1);
this.failed = false;
this.legacyBlocked = false;
this.publish();
},
discard: () => {
if (this.draft === null && !this.failed && !this.legacyBlocked) return;
this.draft = null;
this.failed = false;
this.failDetail = "";
this.legacyBlocked = false;
this.publish();
},
save: () => {
this.save();
}
};
}
async save() {
if (this.draft === null || this.saving) return;
const draft = this.draft;
const ids = draft.servers.map((row) => row.id.trim());
const invalid = ids.some((id) => id === "") || new Set(ids.filter(Boolean)).size !== ids.filter(Boolean).length;
if (invalid) return;
const base = this.baseDraft();
const serversChanged = !deepEqualJson(draft.servers, base.servers);
if (serversChanged) {
const blocked = draft.servers.some((row) => row.legacySecret && row.apiKey.trim() === "" && row.apiKeyEnv.trim() === "");
if (blocked) {
this.legacyBlocked = true;
this.publish();
return;
}
}
this.saving = true;
this.failed = false;
this.failDetail = "";
this.legacyBlocked = false;
this.publish();
let landed = true;
let failDetail = "";
let serverEntries;
const createdCredentialRefs = [];
if (serversChanged) {
serverEntries = [];
const credentialWrites = draft.servers
.filter((row) => row.apiKey.trim() !== "")
.map((row) => ({ ref: credentialRefFor(row), value: row.apiKey.trim() }));
let credentialBefore = {};
try {
if (credentialWrites.length > 0) {
credentialBefore = await describeCredentialRefs(this.remote, [...new Set(credentialWrites.map(({ ref }) => ref))]);
}
} catch (error) {
landed = false;
failDetail = error instanceof Error ? error.message : String(error);
}
if (landed && credentialWrites.some(({ ref }) => credentialBefore[ref]?.writable === false)) {
landed = false;
failDetail = "credential is not writable";
}
if (landed) {
for (const { ref, value } of credentialWrites) {
if (!credentialBefore[ref]?.configured) createdCredentialRefs.push(ref);
try {
const response = await this.remote.credentials.set(ref, value);
if (!response.ok) {
landed = false;
failDetail = response.error?.message || "credentials.set failed";
break;
}
} catch (error) {
landed = false;
failDetail = error instanceof Error ? error.message : String(error);
break;
}
}
}
if (!landed) {
await rollbackNewCredentialRefs(this.remote, createdCredentialRefs);
this.saving = false;
this.failed = true;
this.failDetail = failDetail;
this.publish();
return;
}
for (const row of draft.servers) {
const entry = entryFromRow(row);
if (row.apiKey.trim() !== "") {
delete entry.apiKey;
entry.apiKeyEnv = credentialRefFor(row);
} else if (!entry.apiKeyEnv && (row.url || "").includes("dashscope")) {
entry.apiKeyEnv = "DASHSCOPE_API_KEY";
}
delete entry.legacySecret;
serverEntries.push(entry);
}
}
const ops = [];
const planned = [
["defaultServer", draft.defaultServer.trim(), base.defaultServer],
["maxResults", draft.maxResults.trim() === "" ? undefined : Number(draft.maxResults), base.maxResults === "" ? undefined : Number(base.maxResults)],
["searchTimeoutMs", draft.searchTimeoutMs.trim() === "" ? undefined : Number(draft.searchTimeoutMs), base.searchTimeoutMs === "" ? undefined : Number(base.searchTimeoutMs)],
...(serversChanged ? [["servers", serverEntries, this.value().servers]] : [])
];
for (const [field, value, previous] of planned) {
if (deepEqualJson(value, previous)) continue;
if (value === undefined) ops.push({ op: "unset", path: [field] });
else ops.push({ op: "set", path: [field], value });
}
if (ops.length > 0) {
try {
const revision = this.section().revision;
const response = await this.remote.settings.mutate(NS, ops, revision);
if (!response.ok) {
landed = false;
failDetail = response.error?.message || response.error?.code || "settings.mutate failed";
try {
if (typeof this.describeFace?.load === "function") await this.describeFace.load();
else if (typeof this.describeFace?.ensure === "function") await this.describeFace.ensure();
} catch { /* keep draft */ }
} else if (response.value && typeof this.describeFace?.acceptView === "function") {
this.describeFace.acceptView(response.value);
}
} catch (error) {
landed = false;
failDetail = error instanceof Error ? error.message : String(error);
}
}
if (!landed) {
await rollbackNewCredentialRefs(this.remote, createdCredentialRefs);
} else {
this.draft = null;
}
this.saving = false;
this.failed = !landed;
this.failDetail = landed ? "" : failDetail;
this.publish();
if (landed) this.readSecretState();
}
inject() {
return {
hooks: { searchMcpCard: this.store },
...this.actions()
};
}
};
//#endregion
//#region components
function SearchMcpCard(props) {
const { t } = props;
const [open, setOpen] = (0, react.useState)(false);
const state = props.useSearchMcpCard((snapshot) => snapshot);
if (!state.available) return null;
const disabled = !state.writable;
const value = state.value;
const title = t("title");
const serverOptions = [
{ value: "", label: "—" },
...state.serverIds.map((id) => ({ value: id, label: `${id} · ${kindLabel(state.serverKinds[id], t)}` }))
];
const rowIds = value.servers.map((row) => row.id.trim()).filter(Boolean);
const dupIds = new Set(rowIds.filter((id, index) => rowIds.indexOf(id) !== index));
return (0, react_jsx_runtime.jsxs)("li", {
className: "smcp_card" + (open ? " smcp_cardOpen" : ""),
children: [
(0, react_jsx_runtime.jsxs)("button", {
type: "button",
className: "smcp_headBtn",
"aria-expanded": open,
"aria-label": `${open ? "收起" : "展开"}: ${title}`,
onClick: () => {
setOpen(!open);
},
children: [
(0, react_jsx_runtime.jsxs)("span", {
className: "smcp_head",
children: [
(0, react_jsx_runtime.jsx)("span", { className: "smcp_name", children: title }),
(0, react_jsx_runtime.jsx)("span", { className: "smcp_desc", children: t("description") }),
state.overridden ? (0, react_jsx_runtime.jsx)("span", { className: "smcp_badge", children: t("overridden") }) : null,
state.dirty ? (0, react_jsx_runtime.jsx)("span", { className: "smcp_badgeMuted", children: t("unsaved") }) : null
]
}),
(0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconChevronDownOutline14, {
className: "smcp_chevron" + (open ? " smcp_chevronOpen" : "")
})
]
}),
open ? (0, react_jsx_runtime.jsxs)("div", {
className: "smcp_body",
children: [
!state.writable ? (0, react_jsx_runtime.jsx)("p", { className: "smcp_hint", children: t("readOnly") }) : null,
(0, react_jsx_runtime.jsx)(SelectField, {
id: "smcp-default-server",
label: t("defaultServer"),
hint: t("defaultServerHint"),
disabled,
options: serverOptions,
value: value.defaultServer,
onEdit: (text) => props.editScalar("defaultServer", text)
}),
(0, react_jsx_runtime.jsx)(TextField, {
id: "smcp-max-results",
label: t("maxResults"),
hint: t("maxResultsHint"),
numeric: true,
disabled,
value: value.maxResults,
onEdit: (text) => props.editScalar("maxResults", text)
}),
(0, react_jsx_runtime.jsx)(TextField, {
id: "smcp-timeout",
label: t("searchTimeoutMs"),
hint: t("searchTimeoutMsHint"),
numeric: true,
disabled,
value: value.searchTimeoutMs === "" ? "" : String(Math.round(Number(value.searchTimeoutMs) / 1000)),
onEdit: (text) => props.editScalar("searchTimeoutMs", text === "" ? "" : String(Math.round((Number(text) || 0) * 1000)))
}),
(0, react_jsx_runtime.jsxs)("div", {
className: "smcp_field",
children: [
(0, react_jsx_runtime.jsx)("span", { className: "smcp_label", children: t("servers") }),
(0, react_jsx_runtime.jsx)("p", { className: "smcp_hint", children: t("serversHint") }),
(0, react_jsx_runtime.jsx)("p", { className: "smcp_hint", children: t("quickAddHint") }),
(0, react_jsx_runtime.jsxs)("div", {
className: "smcp_quickAdd",
children: QUICK_KINDS.map((kind) => (0, react_jsx_runtime.jsx)(
"button",
{
type: "button",
className: "smcp_quickBtn",
disabled,
onClick: () => props.quickAdd(kind),
children: `+ ${KIND_LABELS[kind]}`
},
`smcp-quick-${kind}`
))
}),
value.servers.map((row, index) => (0, react_jsx_runtime.jsx)(
ServerRow,
{
t,
row,
index,
disabled,
dupIds,
credentialStates: state.credentialStates,
onEdit: (field, val) => props.editRow(index, field, val),
onKind: (kind) => props.changeKind(index, kind),
onRemove: () => props.removeServer(index)
},
`smcp-row-${index}`
)),
(0, react_jsx_runtime.jsx)("button", {
type: "button",
className: "smcp_add",
disabled,
onClick: props.addServer,
children: `+ ${t("addServer")}`
})
]
}),
state.legacyBlocked ? (0, react_jsx_runtime.jsx)("p", { role: "status", className: "smcp_error", children: t("legacyKeyBlocked") }) : null,
(0, react_jsx_runtime.jsxs)("div", {
className: "smcp_footer",
children: [
state.failed ? (0, react_jsx_runtime.jsx)("p", { role: "status", className: "smcp_failDetail", children: state.failDetail ? (t("saveFailed") + " — " + state.failDetail) : t("saveFailed") }) : null,
(0, react_jsx_runtime.jsx)("button", {
type: "button",
className: "smcp_btn smcp_btnGhost",
disabled: !state.dirty || state.saving,
onClick: props.discard,
children: t("discard")
}),
(0, react_jsx_runtime.jsx)("button", {
type: "button",
className: "smcp_btn smcp_btnPrimary",
disabled: !state.dirty || state.invalid || state.saving,
onClick: props.save,
children: t(state.saving ? "saving" : "save")
})
]
})
]
}) : null
]
});
}
function ServerRow(props) {
const { t, row, index, disabled, onEdit, onKind, onRemove } = props;
const [rowOpen, setRowOpen] = (0, react.useState)(false);
const rowInvalid = row.id.trim() === "";
const dup = !rowInvalid && row.id.trim() !== "" && props.dupIds && props.dupIds.has(row.id.trim());
const known = row.kind !== "custom";
const needsKey = (CATALOG[row.kind] ?? CATALOG.custom).needsKey;
const hasKey = row.apiKey.trim() !== "";
const hasKeyEnv = row.apiKeyEnv.trim() !== "";
const hasConfiguredRef = hasKeyEnv && props.credentialStates?.[row.apiKeyEnv.trim()]?.configured === true;
const hasLegacyKey = row.legacySecret === true;
const overridesGlobal = row.maxResults.trim() !== "";
const summary = known ? (needsKey ? (hasKeyEnv ? `${t("keyRef")}: ${row.apiKeyEnv.trim()}` : t("cdKeyHint")) : t("keyNotRequired")) : (row.transport === "stdio"
? ((row.command.trim() !== "" ? row.command.trim() : "") + (row.args.trim() !== "" ? " " + row.args.trim() : "")).trim() || row.id
: (row.url.trim() !== "" ? row.url.trim() : row.id));
const badges = [];
if (hasKey) badges.push((0, react_jsx_runtime.jsx)("span", { className: "smcp_badge", children: t("keySet") }, "badge-key"));
else if (hasConfiguredRef) badges.push((0, react_jsx_runtime.jsx)("span", { className: "smcp_badge", children: `${t("keyConfigured")}: ${row.apiKeyEnv.trim()}` }, "badge-env"));
else if (hasKeyEnv) badges.push((0, react_jsx_runtime.jsx)("span", { className: "smcp_badgeDanger", children: `${t("keyRefMissing")}: ${row.apiKeyEnv.trim()}` }, "badge-env-missing"));
else if (hasLegacyKey) badges.push((0, react_jsx_runtime.jsx)("span", { className: "smcp_badgeMuted", children: t("legacyKey") }, "badge-legacy-key"));
else if (needsKey) badges.push((0, react_jsx_runtime.jsx)("span", { className: "smcp_badgeDanger", children: t("keyMissing") }, "badge-key-missing"));
if (overridesGlobal) badges.push((0, react_jsx_runtime.jsx)("span", { className: "smcp_badgeMuted", children: t("overridesGlobal") }, "badge-override"));
return (0, react_jsx_runtime.jsxs)("div", {
className: "smcp_row",
children: [
(0, react_jsx_runtime.jsxs)("div", {
className: "smcp_rowHead",
children: [
(0, react_jsx_runtime.jsx)("button", {
type: "button",
className: "smcp_rowToggle" + (rowOpen ? " smcp_rowToggleOpen" : ""),
"aria-expanded": rowOpen,
"aria-label": `${rowOpen ? "收起" : "展开"} ${row.id || t("rowId")}`,
disabled,
onClick: () => setRowOpen(!rowOpen),
children: (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconChevronDownOutline14, {})
}),
(0, react_jsx_runtime.jsx)("span", { className: "smcp_kindBadge", children: kindLabel(row.kind, t) }),
(0, react_jsx_runtime.jsx)("span", { className: "smcp_rowTitle" + (rowInvalid ? " smcp_invalid" : ""), children: row.id.trim() !== "" ? row.id.trim() : t("rowId") }),
(0, react_jsx_runtime.jsx)("span", { className: "smcp_rowSummary", children: summary }),
...badges,
(0, react_jsx_runtime.jsx)("button", { type: "button", className: "smcp_del", disabled, onClick: onRemove, children: t("removeServer") })
]
}),
rowInvalid ? (0, react_jsx_runtime.jsx)("p", { className: "smcp_error", children: t("missingId") }) : null,
dup ? (0, react_jsx_runtime.jsx)("p", { className: "smcp_error", children: t("dupId") }) : null,
rowOpen ? (0, react_jsx_runtime.jsxs)("div", {
className: "smcp_rowBody",
children: known ? [
(0, react_jsx_runtime.jsx)(CellInput, { label: t("rowId"), placeholder: t("rowIdHint"), value: row.id, disabled, onEdit: (v) => onEdit("id", v) }),
(0, react_jsx_runtime.jsxs)("div", { className: "smcp_rowGrid3", children: [
(0, react_jsx_runtime.jsx)(CellSelect, { label: t("rowKind"), options: KIND_OPTIONS, value: row.kind, disabled, onEdit: onKind }),
needsKey ? (0, react_jsx_runtime.jsx)(CellInput, { label: t("cdKey"), type: "password", placeholder: t("cdKeyHint"), value: row.apiKey, disabled, onEdit: (v) => onEdit("apiKey", v) }) : null,
needsKey ? (0, react_jsx_runtime.jsx)(CellInput, { label: t("rowApiKeyEnv"), placeholder: t("rowApiKeyEnv"), value: row.apiKeyEnv, disabled, onEdit: (v) => onEdit("apiKeyEnv", v) }) : null
] }),
(0, react_jsx_runtime.jsx)(CellInput, { label: t("rowMaxResults"), placeholder: t("rowMaxResultsHint"), value: row.maxResults, disabled, onEdit: (v) => onEdit("maxResults", v) })
] : [
(0, react_jsx_runtime.jsx)(CellInput, { label: t("rowId"), placeholder: t("rowIdHint"), value: row.id, disabled, onEdit: (v) => onEdit("id", v) }),
(0, react_jsx_runtime.jsxs)("div", { className: "smcp_rowGrid3", children: [
(0, react_jsx_runtime.jsx)(CellSelect, { label: t("rowKind"), options: KIND_OPTIONS, value: row.kind, disabled, onEdit: onKind }),
(0, react_jsx_runtime.jsx)(CellSelect, { label: t("rowTransport"), options: TRANSPORT_OPTIONS, value: row.transport, disabled, onEdit: (v) => onEdit("transport", v) }),
(0, react_jsx_runtime.jsx)(CellSelect, { label: t("rowAuthStyle"), options: AUTH_STYLE_OPTIONS, value: row.authStyle, disabled, onEdit: (v) => onEdit("authStyle", v) })
] }),
(0, react_jsx_runtime.jsxs)("div", { className: "smcp_rowGrid", children: [
(0, react_jsx_runtime.jsx)(CellInput, { label: t("rowUrl"), placeholder: t("placeholderUrl"), value: row.url, disabled, onEdit: (v) => onEdit("url", v) }),
(0, react_jsx_runtime.jsx)(CellInput, { label: t("rowApiKey"), type: "password", placeholder: t("cdKeyHint"), value: row.apiKey, disabled, onEdit: (v) => onEdit("apiKey", v) })
] }),
(0, react_jsx_runtime.jsxs)("div", { className: "smcp_rowGrid", children: [
(0, react_jsx_runtime.jsx)(CellInput, { label: t("rowApiKeyEnv"), placeholder: t("rowApiKeyEnv"), value: row.apiKeyEnv, disabled, onEdit: (v) => onEdit("apiKeyEnv", v) }),
(0, react_jsx_runtime.jsx)(CellInput, { label: t("rowAuthParam"), placeholder: t("placeholderAuthParam"), value: row.authParam, disabled, onEdit: (v) => onEdit("authParam", v) })
] }),
(0, react_jsx_runtime.jsxs)("div", { className: "smcp_rowGrid", children: [
(0, react_jsx_runtime.jsx)(CellInput, { label: t("rowCommand"), placeholder: t("placeholderCommand"), value: row.command, disabled, onEdit: (v) => onEdit("command", v) }),
(0, react_jsx_runtime.jsx)(CellInput, { label: t("rowArgs"), placeholder: t("placeholderArgs"), value: row.args, disabled, onEdit: (v) => onEdit("args", v) })
] }),
(0, react_jsx_runtime.jsxs)("div", { className: "smcp_rowGrid", children: [
(0, react_jsx_runtime.jsx)(CellInput, { label: t("rowToolName"), placeholder: t("placeholderToolName"), value: row.toolName, disabled, onEdit: (v) => onEdit("toolName", v) }),
(0, react_jsx_runtime.jsx)(CellInput, { label: t("rowMaxResults"), placeholder: t("rowMaxResultsHint"), value: row.maxResults, disabled, onEdit: (v) => onEdit("maxResults", v) })
] })
]
}) : null
]
});
}
function CellInput(props) {
return (0, react_jsx_runtime.jsxs)("label", {
className: "smcp_cell",
children: [
(0, react_jsx_runtime.jsx)("span", { className: "smcp_cellLabel", children: props.label }),
(0, react_jsx_runtime.jsx)("input", {
className: "smcp_input",
type: props.type ?? "text",
placeholder: props.placeholder,
value: props.value,
disabled: props.disabled,
onChange: (e) => props.onEdit(e.target.value)
})
]
});
}
function CellSelect(props) {
return (0, react_jsx_runtime.jsxs)("label", {
className: "smcp_cell",
children: [
(0, react_jsx_runtime.jsx)("span", { className: "smcp_cellLabel", children: props.label }),
(0, react_jsx_runtime.jsx)("select", {
className: "smcp_select",
value: props.value,
disabled: props.disabled,
onChange: (e) => props.onEdit(e.target.value),
children: props.options.map((option) => (0, react_jsx_runtime.jsx)("option", { value: option, children: option === "" ? "—" : option }, option))
})
]
});
}
function TextField(props) {
return (0, react_jsx_runtime.jsxs)("div", {
className: "smcp_field",
children: [
(0, react_jsx_runtime.jsx)("label", { className: "smcp_label", htmlFor: props.id, children: props.label }),
(0, react_jsx_runtime.jsx)("input", {
id: props.id,
className: "smcp_input",
type: "text",
inputMode: props.numeric ? "numeric" : undefined,
value: props.value,
disabled: props.disabled,
onChange: (e) => props.onEdit(e.target.value)
}),
(0, react_jsx_runtime.jsx)("p", { className: "smcp_hint", children: props.hint })
]
});
}
function SelectField(props) {
const options = props.options.map((option) => typeof option === "string" ? { value: option, label: option } : option);
return (0, react_jsx_runtime.jsxs)("div", {
className: "smcp_field",
children: [
(0, react_jsx_runtime.jsx)("label", { className: "smcp_label", htmlFor: props.id, children: props.label }),
(0, react_jsx_runtime.jsx)("select", {
id: props.id,
className: "smcp_select",
value: props.value,
disabled: props.disabled,
onChange: (e) => props.onEdit(e.target.value),
children: options.map((option) => (0, react_jsx_runtime.jsx)("option", { value: option.value, children: option.label }, option.value))
}),
(0, react_jsx_runtime.jsx)("p", { className: "smcp_hint", children: props.hint })
]
});
}
//#endregion
//#region apply
/** Required client services (resolved by the client kernel). */
const inject = ["slots", "locale", "settingsScope", "remote", "remote.credentials", "remote.settings"];
function apply(ctx) {
const t = ctx.locale.bind(NS);
ctx.effect(() => ctx.locale.register(NS, { zh, en }), "dsh-search-mcp: section dictionaries");
const describeFace = ctx.settingsScope.describe();
const controller = new SearchMcpCardController(ctx.settingsScope.bind({ namespace: NS }), ctx.remote, describeFace);
ctx.effect(() => ctx.remote.$on("credentials/reference-updated", (ref) => {
const used = controller.baseDraft().servers.some((row) => row.apiKeyEnv.trim() === ref);
if (used) controller.readSecretState();
}), "dsh-search-mcp: credential status invalidation");
ctx.slots.inject("settings.plugin.item", function* () {
yield ctx.slots.register({
name: "settings.plugin.item",
key: NS,
order: 30,
locale: NS,
inject: () => controller.inject()
}, SearchMcpCard);
});
}
//#endregion
exports.apply = apply;
exports.inject = inject;
return module.exports;
}
});