dsh-mcp-connector 0.2.59 → 0.2.60

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -4,6 +4,18 @@
4
4
 
5
5
  ## [Unreleased]
6
6
 
7
+ ## [0.2.60] - 2026-09-29
8
+
9
+ ### Fixed
10
+
11
+ - 放宽 `@deepseek-ai/dsh-mcp-client` peer 范围以支持 DSH `0.2.x`,避免插件在组合阶段被整体跳过、全部 MCP Server 消失(#101)。
12
+ - 忽略工具 Schema 读取或 restriction 更新期间同步触发的嵌套 `tools/change`,阻断与工具重注册插件组合时的永久重扫和 100% CPU 死循环(#99)。
13
+ - 连接范围选择器列出全部可用 Workspace;没有当前会话时仍可显式选择目标项目,不再只能保存为 profile 全局(#100)。
14
+
15
+ ### Verification
16
+
17
+ - 新增治理与作用域控制器的同步重入回归、无当前会话的 Workspace 列表回归,以及 DSH `0.2.x` peer 范围门禁。
18
+
7
19
  ## [0.2.59] - 2026-09-25
8
20
 
9
21
  ### Fixed
package/README.en.md CHANGED
@@ -198,7 +198,7 @@ npm run dev:ui
198
198
 
199
199
  Every Registry merge regenerates `catalog-stats.json`; an hourly workflow in this repository synchronizes the Chinese and English product copy plus a local stats snapshot. The static npm README updates with package releases, while the live badges above read the Registry directly and therefore stay current without another npm release.
200
200
 
201
- The current public version is [`dsh-mcp-connector@0.2.59`](https://www.npmjs.com/package/dsh-mcp-connector), with [GitHub Release v0.2.59](https://github.com/duhu2000/dsh-mcp-connector/releases/tag/v0.2.59).
201
+ The current public version is [`dsh-mcp-connector@0.2.60`](https://www.npmjs.com/package/dsh-mcp-connector), with [GitHub Release v0.2.60](https://github.com/duhu2000/dsh-mcp-connector/releases/tag/v0.2.60).
202
202
 
203
203
  See [CHANGELOG.md](CHANGELOG.md) for version history and [docs/DESKTOP-E2E.md](docs/DESKTOP-E2E.md) for the Desktop release checklist.
204
204
 
package/README.md CHANGED
@@ -193,7 +193,7 @@ npm run dev:ui
193
193
 
194
194
  公共 Registry 每次合并后会生成 `catalog-stats.json`;本仓库的定时工作流每小时同步中英文介绍和统计快照。npm 页面中的静态正文随版本发布更新,上方动态统计徽标则直接读取 Registry,可在不发布新 npm 版本时保持实时数量一致。
195
195
 
196
- 当前公开版本为 [`dsh-mcp-connector@0.2.59`](https://www.npmjs.com/package/dsh-mcp-connector),对应 [GitHub Release v0.2.59](https://github.com/duhu2000/dsh-mcp-connector/releases/tag/v0.2.59)。
196
+ 当前公开版本为 [`dsh-mcp-connector@0.2.60`](https://www.npmjs.com/package/dsh-mcp-connector),对应 [GitHub Release v0.2.60](https://github.com/duhu2000/dsh-mcp-connector/releases/tag/v0.2.60)。
197
197
 
198
198
  版本能力与变更记录见 [CHANGELOG.md](CHANGELOG.md)。
199
199
  Desktop 发版回归见 [docs/DESKTOP-E2E.md](docs/DESKTOP-E2E.md)。
@@ -13,9 +13,9 @@
13
13
 
14
14
  Desktop 和 Web 使用同一个 Host API:
15
15
 
16
- 1. 市场从父级 DSH 客户端读取当前 Workspace;
16
+ 1. 市场从父级 DSH 客户端和 Host Workspace Registry 读取当前 Workspace 及可用 Workspace 列表;
17
17
  2. OAuth、免密、Bearer/API Key、JSON 导入和 URL 安装都会在提交前显示目标范围;
18
- 3. 已选择 Workspace 时页面默认为“当前项目”,也可显式选择“所有项目(全局)”;
18
+ 3. 已选择 Workspace 时页面默认为“当前项目”;没有当前会话时仍可从列表显式选择任一 Workspace,或选择“所有项目(全局)”;
19
19
  4. 项目范围只在 Host 支持最终执行 Guard,且目标 Workspace 仍存在时才能保存。
20
20
 
21
21
  对话工具的 `scope` 可传 `project` 或 `global`;`project` 同时要求 `workspaceId`。旧版客户端未传作用域时,新连接按兼容规则归为当前 profile 全局。
package/lib/client.js CHANGED
@@ -1013,6 +1013,20 @@ window.__ModuleLoader__.load({
1013
1013
  };
1014
1014
  }
1015
1015
 
1016
+ function availableWorkspaceContexts(ctx) {
1017
+ const items = ctx.workspaces.list.getSnapshot()?.items;
1018
+ if (!Array.isArray(items)) return [];
1019
+ const workspaces = new Map();
1020
+ for (const item of items) {
1021
+ if (typeof item?.workspaceId !== "string" || item.workspaceId === "") continue;
1022
+ workspaces.set(item.workspaceId, {
1023
+ workspaceId: item.workspaceId,
1024
+ title: item.title ?? String(item.workspaceId)
1025
+ });
1026
+ }
1027
+ return [...workspaces.values()];
1028
+ }
1029
+
1016
1030
  async function startPromptSession(ctx, promptText) {
1017
1031
  if (typeof promptText !== "string" || promptText.trim() === "") {
1018
1032
  throw new Error("Prompt 不能为空");
@@ -1054,7 +1068,8 @@ window.__ModuleLoader__.load({
1054
1068
  useStore,
1055
1069
  actions,
1056
1070
  startPromptSession: launchPromptSession,
1057
- workspaceContext = () => null
1071
+ workspaceContext = () => null,
1072
+ workspaceOptions = () => []
1058
1073
  } = props;
1059
1074
  const open = useStore((state) => state.open);
1060
1075
  const detailOpen = useStore((state) => state.detailOpen);
@@ -1078,7 +1093,8 @@ window.__ModuleLoader__.load({
1078
1093
  const sendWorkspaceContext = () => {
1079
1094
  frameRef.current?.contentWindow?.postMessage({
1080
1095
  type: WORKSPACE_CONTEXT_RESULT_TYPE,
1081
- workspace: workspaceContext()
1096
+ workspace: workspaceContext(),
1097
+ workspaces: workspaceOptions()
1082
1098
  }, window.location.origin);
1083
1099
  };
1084
1100
  const onMessage = (event) => {
@@ -1726,7 +1742,8 @@ window.__ModuleLoader__.load({
1726
1742
  store: marketView,
1727
1743
  inject: () => ({
1728
1744
  startPromptSession: (promptText) => startPromptSession(ctx, promptText),
1729
- workspaceContext: () => currentWorkspaceContext(ctx)
1745
+ workspaceContext: () => currentWorkspaceContext(ctx),
1746
+ workspaceOptions: () => availableWorkspaceContexts(ctx)
1730
1747
  })
1731
1748
  }, MarketOverlay);
1732
1749
  });
@@ -313,13 +313,13 @@ export function createHostScopeController(ctx, {
313
313
  }
314
314
 
315
315
  let syncing = false;
316
- let pending = false;
317
316
  function refresh() {
318
317
  if (disposed || !agentsMounted) return;
319
- if (syncing) {
320
- pending = true;
321
- return;
322
- }
318
+ // Reading lazy Host schemas or replacing a restriction may synchronously
319
+ // emit tools/change. Ignore that nested notification: replaying it scans
320
+ // the same single-threaded state and can otherwise keep the event loop in
321
+ // a permanent tools/change cycle with plugins that re-register tools.
322
+ if (syncing) return;
323
323
  syncing = true;
324
324
  try {
325
325
  const live = new Set(mountedAgentCtx?.agents?.list?.() ?? []);
@@ -327,10 +327,6 @@ export function createHostScopeController(ctx, {
327
327
  for (const agent of restrictions.keys()) if (!live.has(agent)) restrictions.delete(agent);
328
328
  } finally {
329
329
  syncing = false;
330
- if (pending) {
331
- pending = false;
332
- refresh();
333
- }
334
330
  }
335
331
  }
336
332
 
package/lib/governance.js CHANGED
@@ -336,13 +336,14 @@ export function createHostGovernanceController(ctx, { getRules, getRecords, logg
336
336
  }
337
337
 
338
338
  let syncing = false;
339
- let pendingSync = false;
340
339
  function syncRestrictions() {
341
340
  if (disposed || !agentsMounted) return;
342
- if (syncing) {
343
- pendingSync = true;
344
- return;
345
- }
341
+ // schemas()/restrict() may synchronously cause another tools/change while
342
+ // Host plugins materialize lazy schemas or re-register equivalent tools.
343
+ // JavaScript cannot interleave an unrelated external change in this stack,
344
+ // so replaying that nested event only scans the same state again and can
345
+ // form an endless tools/change -> schemas -> tools/change loop.
346
+ if (syncing) return;
346
347
  syncing = true;
347
348
  try {
348
349
  const denied = deniedObservedNames();
@@ -353,10 +354,6 @@ export function createHostGovernanceController(ctx, { getRules, getRecords, logg
353
354
  }
354
355
  } finally {
355
356
  syncing = false;
356
- if (pendingSync) {
357
- pendingSync = false;
358
- syncRestrictions();
359
- }
360
357
  }
361
358
  }
362
359
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-mcp-connector",
3
- "version": "0.2.59",
3
+ "version": "0.2.60",
4
4
  "description": "DeepSeek Harness MCP Connector: connect servers, search tools across active connections, and troubleshoot discovery. Includes a continuously updated catalog; supports OAuth 2.0 PKCE, API keys, Streamable HTTP/stdio, and mcpServers JSON import.",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",
@@ -142,7 +142,7 @@
142
142
  "peerDependencies": {
143
143
  "@deepseek-ai/dsh-client-ui-conversation": "*",
144
144
  "@deepseek-ai/dsh-client-ui-layout": "*",
145
- "@deepseek-ai/dsh-mcp-client": "^0.1.1-rc.2",
145
+ "@deepseek-ai/dsh-mcp-client": "^0.1.1-rc.2 || >=0.2.0-rc.1 <0.3.0-0",
146
146
  "@deepseek-ai/dsh-storage-domain": "*",
147
147
  "@deepseek-ai/schemastery": "^3.18.1",
148
148
  "@deepseek-ai/cordis": "^4.0.1",
package/ui/index.html CHANGED
@@ -590,6 +590,7 @@
590
590
  let communityCtaEligible = false;
591
591
  let communityCtaDismissed = false;
592
592
  let currentWorkspace = null;
593
+ let availableWorkspaces = [];
593
594
  let scopeRevision = 0;
594
595
  let scopeRollbackRevision = null;
595
596
  let pendingScopeKey = null;
@@ -660,15 +661,34 @@
660
661
  showToast(message || (ok ? '已带入新会话' : '打开新会话失败,请重试'), ok);
661
662
  }
662
663
 
664
+ function normalizeWorkspaceOptions(input) {
665
+ const byId = new Map();
666
+ for (const item of Array.isArray(input) ? input : []) {
667
+ const workspaceId = typeof item?.workspaceId === 'string' ? item.workspaceId : typeof item?.id === 'string' ? item.id : '';
668
+ if (!workspaceId) continue;
669
+ byId.set(workspaceId, { workspaceId, title: String(item.title || workspaceId) });
670
+ }
671
+ if (currentWorkspace && !byId.has(currentWorkspace.workspaceId)) {
672
+ byId.set(currentWorkspace.workspaceId, currentWorkspace);
673
+ }
674
+ return [...byId.values()];
675
+ }
676
+
677
+ function updateAvailableWorkspaces(input) {
678
+ availableWorkspaces = normalizeWorkspaceOptions(input);
679
+ }
680
+
663
681
  window.addEventListener('message', (event) => {
664
682
  if (event.origin !== window.location.origin || event.source !== window.parent) return;
665
683
  if (event.data?.type === WORKSPACE_CONTEXT_RESULT_TYPE) {
666
684
  const workspace = event.data.workspace;
667
685
  const previousWorkspaceId = currentWorkspace?.workspaceId ?? null;
686
+ const previousWorkspaceOptions = JSON.stringify(availableWorkspaces);
668
687
  currentWorkspace = workspace && typeof workspace.workspaceId === 'string'
669
688
  ? { workspaceId: workspace.workspaceId, title: String(workspace.title || workspace.workspaceId) }
670
689
  : null;
671
- if ((currentWorkspace?.workspaceId ?? null) !== previousWorkspaceId) {
690
+ updateAvailableWorkspaces(event.data.workspaces);
691
+ if ((currentWorkspace?.workspaceId ?? null) !== previousWorkspaceId || JSON.stringify(availableWorkspaces) !== previousWorkspaceOptions) {
672
692
  explorerSequence++; explorerDetailSequence++;
673
693
  explorerData = null; explorerOffset = 0;
674
694
  explorerFilter = { connectionKey: '', serverName: '', status: '' };
@@ -689,16 +709,25 @@
689
709
  }
690
710
 
691
711
  function scopeFieldHtml({ id = 'cfg-scope', selected } = {}) {
692
- const projectAvailable = currentWorkspace !== null;
693
- const value = selected || (projectAvailable ? 'project' : 'global');
694
- return `<label for="${esc(id)}">连接范围</label><select id="${esc(id)}"><option value="project" ${value === 'project' ? 'selected' : ''} ${projectAvailable ? '' : 'disabled'}>${projectAvailable ? `当前项目:${esc(currentWorkspace.title)}` : '当前项目(请先选择 Workspace)'}</option><option value="global" ${value === 'global' ? 'selected' : ''}>所有项目(全局)</option></select><div class="field-help">项目范围只在当前 Workspace 的 Agent 中可见;全局范围由所有 Workspace 继承。范围绑定不保存或复制凭据。</div>`;
712
+ const defaultValue = currentWorkspace ? `project:${currentWorkspace.workspaceId}` : 'global';
713
+ const value = selected === 'project'
714
+ ? defaultValue
715
+ : selected || defaultValue;
716
+ const projectOptions = availableWorkspaces.map((workspace) => {
717
+ const optionValue = `project:${workspace.workspaceId}`;
718
+ const prefix = workspace.workspaceId === currentWorkspace?.workspaceId ? '当前项目' : '项目';
719
+ return `<option value="${esc(optionValue)}" ${value === optionValue ? 'selected' : ''}>${prefix}:${esc(workspace.title)}</option>`;
720
+ }).join('');
721
+ const unavailable = projectOptions ? '' : '<option value="" disabled>暂无可用 Workspace</option>';
722
+ return `<label for="${esc(id)}">连接范围</label><select id="${esc(id)}">${projectOptions}${unavailable}<option value="global" ${value === 'global' ? 'selected' : ''}>所有项目(全局)</option></select><div class="field-help">可选择任一 Workspace;项目范围只在该 Workspace 的 Agent 中可见。全局范围由所有 Workspace 继承,范围绑定不保存或复制凭据。</div>`;
695
723
  }
696
724
 
697
725
  function selectedScopeParams(id = 'cfg-scope') {
698
- const scope = $(`#${id}`)?.value || 'global';
699
- if (scope === 'project') {
700
- if (!currentWorkspace) throw new Error('请先选择一个 Workspace,再使用项目范围');
701
- return { scope, workspaceId: currentWorkspace.workspaceId };
726
+ const value = $(`#${id}`)?.value || 'global';
727
+ if (value.startsWith('project:')) {
728
+ const workspaceId = value.slice('project:'.length);
729
+ if (!workspaceId) throw new Error('请选择一个 Workspace,再使用项目范围');
730
+ return { scope: 'project', workspaceId };
702
731
  }
703
732
  return { scope: 'global' };
704
733
  }
@@ -1133,7 +1162,10 @@
1133
1162
  syncMarketNav();
1134
1163
  main.innerHTML = '<div class="empty">加载中…</div>';
1135
1164
  const [r, status] = await Promise.all([call('catalog', workspaceParams()), call('status', workspaceParams())]);
1136
- if (status.ok) setTabCount('installed', (status.detail?.items ?? []).length);
1165
+ if (status.ok) {
1166
+ updateAvailableWorkspaces(status.detail?.scope?.workspaces);
1167
+ setTabCount('installed', (status.detail?.items ?? []).length);
1168
+ }
1137
1169
  if (!r.ok) { main.innerHTML = `<div class="empty">${esc(r.message)}</div>`; return; }
1138
1170
  const items = r.detail?.items ?? [];
1139
1171
  setTabCount('market', items.length);
@@ -1284,6 +1316,7 @@
1284
1316
  main.innerHTML = '<div class="empty">加载中…</div>';
1285
1317
  const [r, migration] = await Promise.all([call('status', workspaceParams()), call('migrationPreview', { scanStored: false })]);
1286
1318
  if (!r.ok) { main.innerHTML = `<div class="empty">${esc(r.message)}</div>`; return; }
1319
+ updateAvailableWorkspaces(r.detail?.scope?.workspaces);
1287
1320
  const items = r.detail?.items ?? [];
1288
1321
  installedItems = items;
1289
1322
  scopeRevision = Number.isInteger(r.detail?.scope?.revision) ? r.detail.scope.revision : 0;