mirror of
https://github.com/hansjone/dsh-search-mcp.git
synced 2026-10-08 22:20:47 +08:00
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>
83 lines
3.2 KiB
JavaScript
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;
|
|
}
|