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>
This commit is contained in:
oliver 2026-09-12 08:51:32 +08:00
commit d5c4d2006c
16 changed files with 4821 additions and 0 deletions

View file

@ -0,0 +1,79 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import { readFile } from 'node:fs/promises';
import { dirname, resolve } from 'node:path';
import { fileURLToPath } from 'node:url';
const root = resolve(dirname(fileURLToPath(import.meta.url)), '..');
const read = (path) => readFile(resolve(root, path), 'utf8');
test('package exports resolve and RC2 dependencies stay pinned', async () => {
const pkg = JSON.parse(await read('package.json'));
assert.equal(pkg.exports['.'], './lib/index.js');
assert.equal(pkg.exports['./client'], './lib/client.browser.js');
assert.equal(pkg.engines.node, '>=20');
for (const name of [
'@deepseek-ai/dsh-api-remotes',
'@deepseek-ai/dsh-credentials',
'@deepseek-ai/dsh-launch-environment',
'@deepseek-ai/dsh-settings',
'@deepseek-ai/dsh-web',
]) {
assert.equal(pkg.dependencies[name], '0.1.1-rc.2');
}
assert.equal(pkg.dependencies['@deepseek-ai/schemastery'], '3.18.1');
assert.equal(pkg.dependencies.undici, '6.28.0');
assert.equal(pkg.dependencies['ipaddr.js'], '2.5.0');
assert.equal(pkg.dependencies['@modelcontextprotocol/sdk'], '1.30.0');
});
test('RC2 browser bundle uses keyed settings slot and credential migration', async () => {
const client = await read('lib/client.browser.js');
assert.match(client, /name: "settings\.plugin\.item",\s+key: NS,/);
assert.doesNotMatch(client, /name: "settings\.plugin\.item",\s+id:/);
assert.match(client, /api\.settings\.describe\(\{\}\)/);
assert.match(client, /api\.credentials\.describe\(\{ refs: refs\.slice\(index, index \+ CREDENTIAL_DESCRIBE_BATCH_SIZE\) \}\)/);
assert.match(client, /api\.credentials\.set\(\{ ref, value \}\)/);
assert.match(client, /credentials\/reference-updated/);
assert.match(client, /CREDENTIAL_DESCRIBE_BATCH_SIZE = 64/);
assert.match(client, /api\.credentials\.unset\(\{ ref \}\)/);
assert.match(client, /rollbackSettingsWrites/);
assert.match(client, /legacyKeyBlocked/);
assert.match(client, /deepEqualJson\(current\[field\], value\)/);
});
test('known providers are CDKey-only while custom keeps advanced fields', async () => {
const client = await read('lib/client.browser.js');
const catalog = client.slice(client.indexOf('const CATALOG = {'), client.indexOf('const KIND_OPTIONS'));
assert.doesNotMatch(catalog, /https?:\/\//);
assert.doesNotMatch(catalog, /toolName|authParam|transport/);
assert.match(client, /const known = row\.kind !== "custom"/);
assert.match(client, /children: known \? \[/);
assert.match(client, /No endpoint is required for known providers/);
assert.match(client, /已知提供商不需要填写端点链接/);
assert.match(client, /kind, apiKey: "", apiKeyEnv: ""/);
});
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, /validateHttpEndpoint\(server\.url, \{ signal \}\)/);
assert.match(transport, /new Agent\(\{[\s\S]*connect: \{ lookup: validated\.lookup \}/);
assert.match(transport, /requestUrl\.origin !== expectedOrigin/);
assert.match(transport, /dispatcher: agent/);
assert.match(transport, /redirect: 'error'/);
assert.match(transport, /fetch: secureFetch/);
assert.match(transport, /await client\.close\(\)[\s\S]*await runtime\?\.close\(\)/);
assert.doesNotMatch(transport, /new URL\(server\.url\)[\s\S]*new StreamableHTTPClientTransport\(url, \{\s*requestInit:/);
});
test('bundle replaces built-in search and leaves default row endpoint-free', async () => {
const patch = await read('cordis.patch.yml');
assert.match(patch, /searchProvider: search-mcp/);
assert.match(patch, /- id: web-search-deepseek\s+disabled: true/);
assert.match(patch, /- id: tool-web\s+disabled: false/);
assert.match(patch, /fetch: false/);
assert.match(patch, /searchMaxResults: 50/);
assert.match(patch, /searchMaxQueries: 4/);
const defaultRow = patch.slice(patch.indexOf('- id: tavily'), patch.indexOf('- id: web'));
assert.doesNotMatch(defaultRow, /url:|toolName:|authParam:|transport:/);
});

130
test/runtime.test.js Normal file
View file

@ -0,0 +1,130 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import { clampSearchResults, SEARCH_MCP_CATALOG, resolveServer } from '../lib/catalog.js';
import { extractSearchResult } from '../lib/extract.js';
test('catalog exposes every supported provider preset', () => {
assert.deepEqual(Object.keys(SEARCH_MCP_CATALOG), [
'tavily',
'brave',
'exa',
'perplexity',
'duckduckgo',
'custom',
]);
});
test('known providers ignore stored connection overrides', () => {
const tavily = resolveServer({
id: 'primary',
kind: 'tavily',
transport: 'stdio',
url: 'https://example.test/mcp',
authStyle: 'header',
authParam: 'X-Other-Key',
toolName: 'other_search',
apiKeyEnv: 'MY_TAVILY_KEY',
maxResults: 12,
});
assert.equal(tavily.transport, 'http');
assert.equal(tavily.url, 'https://mcp.tavily.com/mcp/');
assert.equal(tavily.authStyle, 'query');
assert.equal(tavily.authParam, 'tavilyApiKey');
assert.equal(tavily.toolName, 'tavily_search');
assert.equal(tavily.countArg, 'max_results');
assert.equal(tavily.apiKeyEnv, 'MY_TAVILY_KEY');
assert.equal(tavily.maxResults, 12);
});
test('prototype property kinds fall back to custom', () => {
for (const kind of ['constructor', 'toString', '__proto__']) {
const resolved = resolveServer({ id: kind, kind, url: 'https://search.example/mcp' });
assert.equal(resolved.kind, 'custom');
assert.equal(resolved.url, 'https://search.example/mcp');
}
});
test('custom providers preserve advanced connection fields', () => {
const custom = resolveServer({
id: 'custom',
kind: 'custom',
transport: 'stdio',
command: 'custom-mcp',
args: ['--stdio'],
authStyle: 'header',
authParam: 'X-Key',
authPrefix: 'Token ',
toolName: 'search',
});
assert.equal(custom.transport, 'stdio');
assert.equal(custom.command, 'custom-mcp');
assert.deepEqual(custom.args, ['--stdio']);
assert.equal(custom.authParam, 'X-Key');
assert.equal(custom.authPrefix, 'Token ');
assert.equal(custom.toolName, 'search');
});
test('provider contracts match current upstream transports', () => {
const brave = resolveServer({ id: 'brave', kind: 'brave' });
assert.equal(brave.transport, 'stdio');
assert.equal(brave.command, 'npx');
assert.deepEqual(brave.args, ['-y', '@brave/brave-search-mcp-server@2.1.3']);
assert.equal(brave.authParam, 'BRAVE_API_KEY');
const perplexity = resolveServer({ id: 'perplexity', kind: 'perplexity' });
assert.equal(perplexity.url, 'https://api.perplexity.ai/mcp');
assert.equal(perplexity.authParam, 'Authorization');
assert.equal(perplexity.authPrefix, 'Bearer ');
assert.equal(perplexity.toolName, 'perplexity_search');
const duckduckgo = resolveServer({ id: 'duckduckgo', kind: 'duckduckgo' });
assert.equal(duckduckgo.needsKey, false);
assert.equal(duckduckgo.toolName, 'duckduckgo_web_search');
assert.equal(duckduckgo.countArg, 'count');
});
test('provider result counts stay within upstream MCP schemas', () => {
assert.equal(clampSearchResults(resolveServer({ id: 't', kind: 'tavily' }), 1), 5);
assert.equal(clampSearchResults(resolveServer({ id: 't', kind: 'tavily' }), 50), 20);
assert.equal(clampSearchResults(resolveServer({ id: 'b', kind: 'brave' }), 50), 20);
assert.equal(clampSearchResults(resolveServer({ id: 'p', kind: 'perplexity' }), 0), 1);
assert.equal(clampSearchResults(resolveServer({ id: 'd', kind: 'duckduckgo' }), 50), 20);
assert.equal(clampSearchResults(resolveServer({ id: 'e', kind: 'exa' }), 50), 50);
});
test('extractSearchResult normalizes, deduplicates, and rejects invalid URLs', () => {
const result = extractSearchResult({
structuredContent: {
answer: 'Summary',
results: [
{ url: 'https://example.com/a', title: 'A', content: 'alpha', published_date: '2026-08-18' },
{ url: 'https://example.com/a', title: 'Duplicate' },
{ url: 'ftp://example.com/ignored', title: 'Ignored' },
],
nested: { url: 'http://example.com/b', description: 'beta' },
},
});
assert.deepEqual(result, {
sources: [
{
url: 'https://example.com/a',
title: 'A',
snippet: 'alpha',
publishedAt: '2026-08-18',
},
{ url: 'http://example.com/b', snippet: 'beta' },
],
truncated: false,
content: 'Summary',
});
});
test('extractSearchResult accepts JSON and plain text MCP blocks', () => {
const json = extractSearchResult({
content: [{ type: 'text', text: '{"results":[{"url":"https://example.com"}]}' }],
});
assert.equal(json.sources.length, 1);
const text = extractSearchResult({ content: [{ type: 'text', text: 'Direct answer' }] });
assert.deepEqual(text, { sources: [], truncated: false, content: 'Direct answer' });
});

158
test/url-policy.test.js Normal file
View file

@ -0,0 +1,158 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import {
createPinnedLookup,
isAllowedEndpointAddress,
isPublicAddress,
validateHttpEndpoint,
} from '../lib/url-policy.js';
const lookup = (records) => (_hostname, options, callback) => {
assert.equal(options.all, true);
queueMicrotask(() => callback(null, records));
};
test('URL policy accepts HTTP(S) with public DNS only', async () => {
const result = await validateHttpEndpoint('https://search.example/mcp', {
lookup: lookup([
{ address: '8.8.8.8', family: 4 },
{ address: '2606:4700:4700:0:0:0:0:1111', family: 6 },
]),
});
assert.equal(result.url.hostname, 'search.example');
assert.deepEqual(result.addresses, [
{ address: '8.8.8.8', family: 4 },
{ address: '2606:4700:4700:0:0:0:0:1111', family: 6 },
]);
});
test('URL policy rejects schemes, userinfo, localhost, and ambiguous IPv4', async () => {
for (const input of [
'ftp://example.com/mcp',
'https://user:pass@example.com/mcp',
'http://localhost/mcp',
'http://api.localhost/mcp',
'http://127.0.0.1/mcp',
'http://127.1/mcp',
'http://0177.0.0.1/mcp',
'http://0x7f000001/mcp',
]) {
await assert.rejects(() => validateHttpEndpoint(input), { name: 'SearchMcpUrlPolicyError' }, input);
}
});
test('URL policy rejects private, reserved, test, mapped, and transition ranges', () => {
for (const address of [
'0.0.0.0',
'10.0.0.1',
'100.64.0.1',
'127.0.0.1',
'169.254.169.254',
'172.16.0.1',
'192.168.0.1',
'192.0.2.1',
'198.18.0.1',
'198.51.100.1',
'203.0.113.1',
'224.0.0.1',
'255.255.255.255',
'::',
'::1',
'::ffff:127.0.0.1',
'::7f00:1',
'::a00:1',
'::a9fe:a9fe',
'::c0a8:101',
'::808:808',
'64:ff9b::808:808',
'2001:db8::1',
'2001::1',
'2002:0808:0808::1',
'fc00::1',
'fe80::1',
'ff02::1',
]) {
assert.equal(isPublicAddress(address), false, address);
}
assert.equal(isPublicAddress('8.8.8.8'), true);
assert.equal(isPublicAddress('2606:4700:4700::1111'), true);
});
test('URL policy drops private DNS answers and keeps public ones', async () => {
const result = await validateHttpEndpoint('https://search.example/mcp', {
lookup: lookup([
{ address: '8.8.8.8', family: 4 },
{ address: '10.0.0.1', family: 4 },
]),
});
assert.deepEqual(result.addresses, [{ address: '8.8.8.8', family: 4 }]);
});
test('URL policy accepts Clash fake-IP answers with optional public peers', async () => {
const mixed = await validateHttpEndpoint('https://dashscope.example/mcp', {
lookup: lookup([
{ address: '198.18.2.146', family: 4 },
{ address: '2408:400a:3e:ef02:12f:bd95:e827:51d', family: 6 },
]),
});
assert.deepEqual(mixed.addresses, [
{ address: '198.18.2.146', family: 4 },
{ address: '2408:400a:3e:ef02:12f:bd95:e827:51d', family: 6 },
]);
const fakeOnly = await validateHttpEndpoint('https://dashscope.example/mcp', {
lookup: lookup([{ address: '198.18.2.146', family: 4 }]),
});
assert.deepEqual(fakeOnly.addresses, [{ address: '198.18.2.146', family: 4 }]);
assert.equal(isAllowedEndpointAddress('198.18.2.146'), true);
assert.equal(isAllowedEndpointAddress('10.0.0.1'), false);
});
test('URL policy rejects private-only DNS answers', async () => {
await assert.rejects(
() => validateHttpEndpoint('https://search.example/mcp', {
lookup: lookup([{ address: '10.0.0.1', family: 4 }]),
}),
/non-public address/,
);
});
test('URL policy rejects DNS errors and empty answers without leaking endpoint data', async () => {
await assert.rejects(
() => validateHttpEndpoint('https://search.example/mcp', {
lookup: (_hostname, _options, callback) => callback(new Error('resolver detail')),
}),
/resolution failed/,
);
await assert.rejects(
() => validateHttpEndpoint('https://search.example/mcp', { lookup: lookup([]) }),
/did not resolve/,
);
});
test('URL policy aborts a pending DNS lookup', async () => {
const controller = new AbortController();
const pending = validateHttpEndpoint('https://search.example/mcp', {
lookup: () => {},
signal: controller.signal,
});
controller.abort();
await assert.rejects(pending, /validation was aborted/);
});
test('pinned lookup serves only validated host and addresses', async () => {
const pinned = createPinnedLookup('search.example', [
{ address: '8.8.8.8', family: 4 },
{ address: '2606:4700:4700:0:0:0:0:1111', family: 6 },
]);
const all = await new Promise((resolve, reject) => {
pinned('search.example', { all: true }, (error, records) => error ? reject(error) : resolve(records));
});
assert.deepEqual(all, [
{ address: '8.8.8.8', family: 4 },
{ address: '2606:4700:4700:0:0:0:0:1111', family: 6 },
]);
await assert.rejects(new Promise((resolve, reject) => {
pinned('other.example', {}, (error, address) => error ? reject(error) : resolve(address));
}), /unvalidated hostname/);
});