Files
memind/tkmind-search-mcp.mjs

48 lines
3.8 KiB
JavaScript

import readline from 'node:readline';
import { normalizeMindSearchConfig } from './mindsearch-config.mjs';
import { SEARCH_ERROR_CODES, validateSearchRequest } from './search-capability.mjs';
import { readSafeUrl, searchGithubCode, searchSearxng } from './mindsearch-providers.mjs';
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,
},
});
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'] } },
];
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);
if (checked.value.type === 'code' && config.providers.github) {
return searchGithubCode(checked.value.query, { limit: checked.value.limit }).then((results) => response(id, { content: [{ type: 'text', text: JSON.stringify({ results, query: checked.value.query, provider: 'github' }) }], structuredContent: { results, query: checked.value.query, provider: 'github' } })).catch((err) => error(id, SEARCH_ERROR_CODES.PROVIDER_UNAVAILABLE, err.message));
}
if (config.providers.searxng) {
return searchSearxng(checked.value.query, { limit: checked.value.limit }).then((results) => response(id, { content: [{ type: 'text', text: JSON.stringify({ results, query: checked.value.query, provider: 'searxng' }) }], structuredContent: { results, query: checked.value.query, provider: 'searxng' } })).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' && config.providers.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));
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'); } });