139 lines
8.2 KiB
JavaScript
139 lines
8.2 KiB
JavaScript
import readline from 'node:readline';
|
|
import { normalizeMindSearchConfig, resolveMindSearchService } from './mindsearch-config.mjs';
|
|
import { SEARCH_ERROR_CODES, validateSearchRequest } from './search-capability.mjs';
|
|
import {
|
|
cancelResearchTask,
|
|
getResearchTask,
|
|
readSafeUrl,
|
|
searchGithubCode,
|
|
searchResearchService,
|
|
searchSearxng,
|
|
startResearchTask,
|
|
} from './mindsearch-providers.mjs';
|
|
|
|
function parseJson(value, fallback) {
|
|
if (!value) return fallback;
|
|
try { return JSON.parse(value); } catch { return fallback; }
|
|
}
|
|
|
|
const config = normalizeMindSearchConfig({
|
|
enabled: process.env.TKMIND_SEARCH_ENABLED,
|
|
mode: process.env.TKMIND_SEARCH_MODE || (/^(1|true|yes|on)$/i.test(process.env.TKMIND_SEARCH_ENABLED ?? '') ? 'assist' : 'off'),
|
|
providers: {
|
|
searxng: process.env.TKMIND_SEARCH_PROVIDER_SEARXNG_ENABLED,
|
|
github: process.env.TKMIND_SEARCH_PROVIDER_GITHUB_ENABLED,
|
|
reader: process.env.TKMIND_SEARCH_READER_ENABLED,
|
|
},
|
|
settings: {
|
|
searxngEndpoint: process.env.TKMIND_SEARCH_SEARXNG_URL,
|
|
maxResults: process.env.TKMIND_SEARCH_MAX_RESULTS,
|
|
timeoutMs: process.env.TKMIND_SEARCH_TIMEOUT_MS,
|
|
readerMaxChars: process.env.TKMIND_SEARCH_READER_MAX_CHARS,
|
|
},
|
|
services: parseJson(process.env.TKMIND_SEARCH_SERVICES_JSON, undefined),
|
|
routes: parseJson(process.env.TKMIND_SEARCH_ROUTES_JSON, undefined),
|
|
});
|
|
|
|
const tools = [
|
|
{ name: 'tkmind_search', description: 'Optional external search enhancement; existing web search remains primary.', inputSchema: { type: 'object', properties: { query: { type: 'string' }, type: { type: 'string' }, limit: { type: 'number' } }, required: ['query'] } },
|
|
{ name: 'tkmind_read', description: 'Optional safe URL reader for search citations.', inputSchema: { type: 'object', properties: { url: { type: 'string' } }, required: ['url'] } },
|
|
];
|
|
if (resolveMindSearchService(config, 'research')) {
|
|
tools.push({
|
|
name: 'tkmind_research',
|
|
description: 'Start an asynchronous deep-research task and return its task identifier.',
|
|
inputSchema: {
|
|
type: 'object',
|
|
properties: { question: { type: 'string' }, depth: { type: 'string', enum: ['quick', 'standard', 'deep'] } },
|
|
required: ['question'],
|
|
},
|
|
});
|
|
tools.push(
|
|
{
|
|
name: 'tkmind_research_status',
|
|
description: 'Get Deep Search progress, evidence sources, and the completed report.',
|
|
inputSchema: {
|
|
type: 'object',
|
|
properties: { task_id: { type: 'string' } },
|
|
required: ['task_id'],
|
|
},
|
|
},
|
|
{
|
|
name: 'tkmind_research_cancel',
|
|
description: 'Cancel a queued or running Deep Search task.',
|
|
inputSchema: {
|
|
type: 'object',
|
|
properties: { task_id: { type: 'string' } },
|
|
required: ['task_id'],
|
|
},
|
|
},
|
|
);
|
|
}
|
|
|
|
function response(id, result) { process.stdout.write(`${JSON.stringify({ jsonrpc: '2.0', id, result })}\n`); }
|
|
function error(id, code, message) { response(id, { isError: true, content: [{ type: 'text', text: `${code}: ${message}` }], structuredContent: { code, message } }); }
|
|
function handle(message) {
|
|
const { id, method, params = {} } = message;
|
|
if (method === 'initialize') return response(id, { protocolVersion: '2024-11-05', capabilities: { tools: {} }, serverInfo: { name: 'tkmind-search', version: '0.1.0' } });
|
|
if (method === 'notifications/initialized') return;
|
|
if (method === 'tools/list') return response(id, { tools });
|
|
if (method !== 'tools/call') return error(id, 'METHOD_NOT_FOUND', `unsupported method: ${method}`);
|
|
if (!config.enabled || config.mode === 'off') return error(id, SEARCH_ERROR_CODES.CAPABILITY_DISABLED, 'MindSearch is disabled; existing search capabilities are unchanged.');
|
|
const name = params.name;
|
|
if (name === 'tkmind_search') {
|
|
const checked = validateSearchRequest(params.arguments ?? {});
|
|
if (!checked.ok) return error(id, checked.code, checked.message);
|
|
const route = checked.value.type === 'code' ? 'code' : checked.value.type === 'news' ? 'news' : 'web';
|
|
const service = resolveMindSearchService(config, route);
|
|
if (service?.adapter === 'github') {
|
|
return searchGithubCode(checked.value.query, { limit: checked.value.limit, endpoint: service.endpoint || undefined }).then((results) => response(id, { content: [{ type: 'text', text: JSON.stringify({ results, query: checked.value.query, provider: service.id }) }], structuredContent: { results, query: checked.value.query, provider: service.id } })).catch((err) => error(id, SEARCH_ERROR_CODES.PROVIDER_UNAVAILABLE, err.message));
|
|
}
|
|
if (service?.adapter === 'searxng') {
|
|
return searchSearxng(checked.value.query, { limit: checked.value.limit, endpoint: service.endpoint }).then((results) => response(id, { content: [{ type: 'text', text: JSON.stringify({ results, query: checked.value.query, provider: service.id }) }], structuredContent: { results, query: checked.value.query, provider: service.id } })).catch((err) => error(id, SEARCH_ERROR_CODES.PROVIDER_UNAVAILABLE, err.message));
|
|
}
|
|
if (service?.adapter === 'research-http') {
|
|
return searchResearchService(checked.value.query, { endpoint: service.endpoint, type: checked.value.type, limit: checked.value.limit, timeoutMs: service.timeoutMs }).then((results) => response(id, { content: [{ type: 'text', text: JSON.stringify({ results, query: checked.value.query, provider: service.id }) }], structuredContent: { results, query: checked.value.query, provider: service.id } })).catch((err) => error(id, SEARCH_ERROR_CODES.PROVIDER_UNAVAILABLE, err.message));
|
|
}
|
|
return error(id, SEARCH_ERROR_CODES.PROVIDER_UNAVAILABLE, 'No search provider is configured.');
|
|
}
|
|
if (name === 'tkmind_read' && resolveMindSearchService(config, 'read')?.adapter === 'reader') return readSafeUrl(params.arguments?.url).then((result) => response(id, { content: [{ type: 'text', text: result.content }], structuredContent: result })).catch((err) => error(id, SEARCH_ERROR_CODES.PROVIDER_UNAVAILABLE, err.message));
|
|
if (name === 'tkmind_research') {
|
|
const question = String(params.arguments?.question ?? '').trim();
|
|
if (!question || question.length > 2000) return error(id, SEARCH_ERROR_CODES.INVALID_REQUEST, 'question must be 1-2000 characters');
|
|
const depth = ['quick', 'standard', 'deep'].includes(params.arguments?.depth) ? params.arguments.depth : 'standard';
|
|
const service = resolveMindSearchService(config, 'research');
|
|
if (service?.adapter !== 'research-http') return error(id, SEARCH_ERROR_CODES.PROVIDER_UNAVAILABLE, 'No research service is configured.');
|
|
return startResearchTask(question, {
|
|
endpoint: service.endpoint,
|
|
depth,
|
|
userId: process.env.TKMIND_SEARCH_USER_ID || null,
|
|
llmProviderKeyId: service.llmProviderKeyId || '',
|
|
llmModel: service.llmModel || '',
|
|
timeoutMs: service.timeoutMs,
|
|
})
|
|
.then((result) => response(id, { content: [{ type: 'text', text: JSON.stringify(result) }], structuredContent: result }))
|
|
.catch((err) => error(id, SEARCH_ERROR_CODES.PROVIDER_UNAVAILABLE, err.message));
|
|
}
|
|
if (name === 'tkmind_research_status' || name === 'tkmind_research_cancel') {
|
|
const taskId = String(params.arguments?.task_id ?? '').trim();
|
|
if (!taskId || taskId.length > 200) return error(id, SEARCH_ERROR_CODES.INVALID_REQUEST, 'task_id must be 1-200 characters');
|
|
const service = resolveMindSearchService(config, 'research');
|
|
if (service?.adapter !== 'research-http') return error(id, SEARCH_ERROR_CODES.PROVIDER_UNAVAILABLE, 'No research service is configured.');
|
|
const operation = name === 'tkmind_research_cancel' ? cancelResearchTask : getResearchTask;
|
|
return operation(taskId, {
|
|
endpoint: service.endpoint,
|
|
userId: process.env.TKMIND_SEARCH_USER_ID || null,
|
|
timeoutMs: service.timeoutMs,
|
|
})
|
|
.then((result) => response(id, {
|
|
content: [{ type: 'text', text: result.report || JSON.stringify(result) }],
|
|
structuredContent: result,
|
|
}))
|
|
.catch((err) => error(id, SEARCH_ERROR_CODES.PROVIDER_UNAVAILABLE, err.message));
|
|
}
|
|
return error(id, SEARCH_ERROR_CODES.PROVIDER_UNAVAILABLE, 'No reader provider is configured.');
|
|
}
|
|
|
|
const rl = readline.createInterface({ input: process.stdin });
|
|
rl.on('line', (line) => { try { handle(JSON.parse(line)); } catch { error(null, SEARCH_ERROR_CODES.INVALID_REQUEST, 'invalid JSON-RPC request'); } });
|