dsh-search-mcp/lib/provider.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

83 lines
3.2 KiB
JavaScript

/**
* The `search-mcp` web search provider.
*
* Registers into `ctx.web` under the stable id `search-mcp`; the profile
* patch switches `web.searchProvider` to this id and disables the built-in
* DeepSeek provider, so the model-facing `web_search` tool executes entirely
* through the configured search MCP server(s).
*/
import { WebError } from '@deepseek-ai/dsh-web';
import { resolveServer } from './catalog.js';
import { callMcpSearch } from './client.js';
import { extractSearchResult } from './extract.js';
/** Stable provider id the `web` row's `searchProvider` config selects. */
export const SEARCH_MCP_PROVIDER_ID = 'search-mcp';
/** The web search provider served by this plugin. */
export class SearchMCPProvider {
id = SEARCH_MCP_PROVIDER_ID;
/**
* @param resolveOptions - snapshots the authoritative config (row config,
* or the live settings section) at the START of each operation, so one
* search never mixes two settings saves.
*/
constructor(resolveOptions) {
this.resolveOptions = resolveOptions;
}
/** Usable when at least one server entry exists; precise errors surface at search time. */
available() {
const options = this.resolveOptions();
return Array.isArray(options.servers) && options.servers.length > 0;
}
async search(request, signal) {
const options = this.resolveOptions();
const server = pickServer(options);
const resolved = resolveServer(server);
const maxResults = resolved.maxResults ?? options.maxResults ?? request.maxResults ?? 8;
const key = await options.resolveKey(resolved);
if (resolved.needsKey && (key === undefined || key.length === 0)) {
const ref = resolved.apiKeyEnv.length > 0 ? resolved.apiKeyEnv : 'apiKey';
throw new WebError(
`search-mcp server "${resolved.id}" (${resolved.kind}) has no API key; set "apiKey" or a resolvable "apiKeyEnv" (${ref}) in Settings → Plugins → search-mcp`,
'WEB_PROVIDER_ERROR',
);
}
const combined = buildSignal(signal, options.searchTimeoutMs);
const outcome = await callMcpSearch(resolved, key, { query: request.query, maxResults }, combined);
return extractSearchResult(outcome);
}
}
/** Select the default server, falling back to the first configured entry. */
function pickServer(options) {
const servers = Array.isArray(options.servers) ? options.servers : [];
if (servers.length === 0) {
throw new WebError(
'search-mcp: no search MCP servers configured; add one in Settings → Plugins → search-mcp',
'WEB_PROVIDER_ERROR',
);
}
if (options.defaultServer !== undefined && options.defaultServer.length > 0) {
const found = servers.find((entry) => entry.id === options.defaultServer);
if (found === undefined) {
throw new WebError(
`search-mcp: defaultServer "${options.defaultServer}" is not configured; known servers: ${servers
.map((entry) => `"${entry.id}"`)
.join(', ') || '(none)'}`,
'WEB_PROVIDER_ERROR',
);
}
return found;
}
return servers[0];
}
/** Combine the caller's cancellation with the configured timeout. */
function buildSignal(signal, timeoutMs) {
const timeout = AbortSignal.timeout(timeoutMs);
return signal !== undefined ? AbortSignal.any([signal, timeout]) : timeout;
}