Migrate search-mcp to @modelcontextprotocol/client for Desktop 0.2.

Replace the removed monolith SDK import so link/Desktop hosts can resolve MCP from the 0.2 package graph instead of failing import with MODULE_NOT_FOUND.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
oliver 2026-10-10 11:54:54 +08:00
parent 9d7de03933
commit 229a36d5f3
5 changed files with 99 additions and 1062 deletions

View file

@ -2,7 +2,8 @@
用搜索类 MCP 服务器完整替代 DeepSeek Harness(DSH)内置网页搜索的独立插件。
> 当前兼容基线:DeepSeek Harness `0.1.1-rc.2`,Node.js 20 或更高版本。
> 当前兼容基线:DeepSeek Harness `>=0.2.0-rc.2`(Desktop / Web),Node.js 20 或更高版本。
> MCP 传输使用 DSH 0.2 提供的 `@modelcontextprotocol/client`(不再依赖已从 Desktop 移除的 monolith `@modelcontextprotocol/sdk`)。
## 功能
@ -17,14 +18,14 @@
## 安装
```powershell
git clone https://github.com/gxpppp/dsh-search-mcp.git
git clone https://github.com/hansjone/dsh-search-mcp.git
cd dsh-search-mcp
npm install
dsh plugin --profile web add link:<dsh-search-mcp 的绝对路径>
dsh web
dsh plugin --profile desktop add link:<dsh-search-mcp 的绝对路径>
# 或: dsh plugin --profile web add link:<绝对路径>
```
`link:` 会让源码更新直接作用于 profile。修改或升级浏览器 bundle 后需要重启 DSH Web 并刷新页面。
`link:` 会让源码更新直接作用于 profile。Desktop 0.2 可从宿主解析 `@modelcontextprotocol/client` / `undici` / `ipaddr.js` peers;但仍建议在仓库内跑一次 `npm install`(并删掉空的 `node_modules\@modelcontextprotocol` 残留目录),避免解析踩空目录。修改或升级浏览器 bundle 后需要重启 DSH 并刷新页面。
如果 profile 中已有独立搜索 MCP 行,建议先移除重复入口,避免同时暴露 `mcp__...` 工具和本插件提供的 `web_search`。

View file

@ -1,7 +1,12 @@
/** MCP transport layer for one search. */
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js';
import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js';
/**
* MCP transport layer for one search.
*
* Desktop 0.2 ships the split MCP client package (v2). Import that package so
* link:/Desktop profiles can resolve it from the host graph without a local
* `node_modules` copy of the legacy monolith SDK.
*/
import { Client, StreamableHTTPClientTransport } from '@modelcontextprotocol/client';
import { StdioClientTransport } from '@modelcontextprotocol/client/stdio';
import { WebError } from '@deepseek-ai/dsh-web';
import { Agent, fetch as undiciFetch } from 'undici';
import { clampSearchResults } from './catalog.js';
@ -17,7 +22,10 @@ export async function callMcpSearch(server, key, args, signal) {
}
let runtime;
const client = new Client({ name: 'dsh-search-mcp', version: '0.2.0' }, { capabilities: {} });
const client = new Client(
{ name: 'dsh-search-mcp', version: '0.2.39' },
{ capabilities: {}, versionNegotiation: { mode: 'auto' } },
);
try {
runtime = server.transport === 'stdio'
? { transport: stdioTransport(server, key), close: async () => {} }

1101
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -1,6 +1,6 @@
{
"name": "dsh-search-mcp",
"version": "0.2.38",
"version": "0.2.39",
"description": "Replace dsh's built-in web search with search MCP servers (Tavily / Brave / Exa / Perplexity / DuckDuckGo / custom), configured from the web Settings page. When this plugin is enabled the built-in DeepSeek search provider is disabled.",
"type": "module",
"main": "lib/index.js",
@ -48,7 +48,7 @@
}
},
"dependencies": {
"@modelcontextprotocol/sdk": "1.30.0",
"@modelcontextprotocol/client": "2.0.0",
"ipaddr.js": "2.5.0",
"undici": "6.28.0"
},
@ -58,6 +58,20 @@
"@deepseek-ai/dsh-launch-environment": "*",
"@deepseek-ai/dsh-settings": "*",
"@deepseek-ai/dsh-web": "*",
"@deepseek-ai/schemastery": "*"
"@deepseek-ai/schemastery": "*",
"@modelcontextprotocol/client": "*",
"ipaddr.js": "*",
"undici": "*"
},
"peerDependenciesMeta": {
"@modelcontextprotocol/client": {
"optional": true
},
"ipaddr.js": {
"optional": true
},
"undici": {
"optional": true
}
}
}

View file

@ -12,8 +12,10 @@ test('package exports resolve and peerDependencies stay open', async () => {
assert.equal(pkg.exports['.'], './lib/index.js')
assert.equal(pkg.exports['./client'], './lib/client.browser.js')
assert.equal(pkg.engines.node, '>=20')
assert.equal(pkg.version, '0.2.38')
assert.equal(pkg.version, '0.2.39')
assert.equal(pkg.dsh.client.immediately, false)
assert.equal(pkg.dependencies['@modelcontextprotocol/client'], '2.0.0')
assert.ok(!Object.hasOwn(pkg.dependencies, '@modelcontextprotocol/sdk'))
for (const name of [
'@deepseek-ai/dsh-api-remotes',
@ -22,6 +24,9 @@ test('package exports resolve and peerDependencies stay open', async () => {
'@deepseek-ai/dsh-settings',
'@deepseek-ai/dsh-web',
'@deepseek-ai/schemastery',
'@modelcontextprotocol/client',
'ipaddr.js',
'undici',
]) {
assert.equal(pkg.peerDependencies[name], '*')
}
@ -82,9 +87,13 @@ test('known providers are CDKey-only while custom keeps advanced fields', async
test('HTTP transport pins DNS and applies one guarded fetch to every SDK request', async () => {
const transport = await read('lib/client.js')
assert.match(transport, /from '@modelcontextprotocol\/client'/)
assert.match(transport, /from '@modelcontextprotocol\/client\/stdio'/)
assert.doesNotMatch(transport, /from ['"]@modelcontextprotocol\/sdk/)
assert.match(transport, /validateHttpEndpoint\(server\.url, \{ signal \}\)/)
assert.match(transport, /dispatcher: agent/)
assert.match(transport, /redirect: 'error'/)
assert.match(transport, /versionNegotiation:\s*\{\s*mode:\s*['"]auto['"]/)
})
test('bundle inserts search-mcp without pinning web.searchProvider (Desktop 0.2 boot-safe)', async () => {