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, searchGithubGateway, 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') { const operation = process.env.TKMIND_SEARCH_GITHUB_GATEWAY_URL ? searchGithubGateway : searchGithubCode; return operation(checked.value.query, { limit: checked.value.limit, endpoint: process.env.TKMIND_SEARCH_GITHUB_GATEWAY_URL || 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'); } });