2026-07-15 13:46:17 +08:00
import readline from 'node:readline' ;
2026-07-23 21:38:36 +08:00
import { normalizeMindSearchConfig , resolveMindSearchService } from './mindsearch-config.mjs' ;
2026-07-15 13:46:17 +08:00
import { SEARCH _ERROR _CODES , validateSearchRequest } from './search-capability.mjs' ;
2026-09-09 22:05:02 +08:00
import {
applyMcpCompaction ,
ctxFetchToolDefinition ,
fetchCompactPayload ,
isMcpCompactActive ,
} from './mcp-result-compactor.mjs' ;
2026-07-23 21:38:36 +08:00
import {
2026-07-23 22:27:07 +08:00
cancelResearchTask ,
getResearchTask ,
2026-07-23 21:38:36 +08:00
readSafeUrl ,
searchGithubCode ,
2026-07-24 09:53:06 +08:00
searchGithubGateway ,
2026-07-23 21:38:36 +08:00
searchResearchService ,
searchSearxng ,
startResearchTask ,
} from './mindsearch-providers.mjs' ;
function parseJson ( value , fallback ) {
if ( ! value ) return fallback ;
try { return JSON . parse ( value ) ; } catch { return fallback ; }
}
2026-07-15 13:46:17 +08:00
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 ,
} ,
2026-07-23 21:38:36 +08:00
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 ) ,
2026-07-15 13:46:17 +08:00
} ) ;
2026-09-09 22:05:02 +08:00
const baseTools = [
2026-07-15 13:46:17 +08:00
{ 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' ] } } ,
] ;
2026-07-23 21:38:36 +08:00
if ( resolveMindSearchService ( config , 'research' ) ) {
2026-09-09 22:05:02 +08:00
baseTools . push ( {
2026-07-23 21:38:36 +08:00
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' ] ,
} ,
} ) ;
2026-09-09 22:05:02 +08:00
baseTools . push (
2026-07-23 22:27:07 +08:00
{
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' ] ,
} ,
} ,
) ;
2026-07-23 21:38:36 +08:00
}
2026-07-15 13:46:17 +08:00
2026-09-09 22:05:02 +08:00
function listTools ( ) {
return isMcpCompactActive ( ) ? [ ... baseTools , ctxFetchToolDefinition ] : [ ... baseTools ] ;
}
async function compactToolText ( text , kind ) {
const result = await applyMcpCompaction ( text , { kind , env : process . env } ) ;
return result . text ;
}
2026-07-15 13:46:17 +08:00
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 ;
2026-09-09 22:05:02 +08:00
if ( method === 'tools/list' ) return response ( id , { tools : listTools ( ) } ) ;
2026-07-15 13:46:17 +08:00
if ( method !== 'tools/call' ) return error ( id , 'METHOD_NOT_FOUND' , ` unsupported method: ${ method } ` ) ;
const name = params . name ;
2026-09-09 22:05:02 +08:00
if ( name === 'ctx_fetch' ) {
const handle = String ( params . arguments ? . handle ? ? '' ) . trim ( ) ;
if ( ! handle ) return error ( id , SEARCH _ERROR _CODES . INVALID _REQUEST , 'handle is required' ) ;
return fetchCompactPayload ( handle , { env : process . env } )
. then ( ( result ) => {
if ( ! result . ok ) return error ( id , SEARCH _ERROR _CODES . INVALID _REQUEST , result . message ? ? 'handle not found' ) ;
return response ( id , {
content : [ { type : 'text' , text : result . text } ] ,
structuredContent : { handle : result . handle , restored : true } ,
} ) ;
} )
. catch ( ( err ) => error ( id , SEARCH _ERROR _CODES . PROVIDER _UNAVAILABLE , err . message ) ) ;
}
if ( ! config . enabled || config . mode === 'off' ) return error ( id , SEARCH _ERROR _CODES . CAPABILITY _DISABLED , 'MindSearch is disabled; existing search capabilities are unchanged.' ) ;
2026-07-15 13:46:17 +08:00
if ( name === 'tkmind_search' ) {
const checked = validateSearchRequest ( params . arguments ? ? { } ) ;
if ( ! checked . ok ) return error ( id , checked . code , checked . message ) ;
2026-07-23 21:38:36 +08:00
const route = checked . value . type === 'code' ? 'code' : checked . value . type === 'news' ? 'news' : 'web' ;
const service = resolveMindSearchService ( config , route ) ;
if ( service ? . adapter === 'github' ) {
2026-07-24 09:53:06 +08:00
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 ) ) ;
2026-07-23 21:38:36 +08:00
}
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 ) ) ;
2026-07-15 13:46:17 +08:00
}
2026-07-23 21:38:36 +08:00
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 ) ) ;
2026-07-15 13:46:17 +08:00
}
return error ( id , SEARCH _ERROR _CODES . PROVIDER _UNAVAILABLE , 'No search provider is configured.' ) ;
}
2026-09-09 22:05:02 +08:00
if ( name === 'tkmind_read' && resolveMindSearchService ( config , 'read' ) ? . adapter === 'reader' ) {
return readSafeUrl ( params . arguments ? . url )
. then ( async ( result ) => {
const text = await compactToolText ( result . content , 'tkmind_read' ) ;
return response ( id , {
content : [ { type : 'text' , text } ] ,
structuredContent : { ... result , content : text } ,
} ) ;
} )
. catch ( ( err ) => error ( id , SEARCH _ERROR _CODES . PROVIDER _UNAVAILABLE , err . message ) ) ;
}
2026-07-23 21:38:36 +08:00
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.' ) ;
2026-07-23 22:27:07 +08:00
return startResearchTask ( question , {
endpoint : service . endpoint ,
depth ,
userId : process . env . TKMIND _SEARCH _USER _ID || null ,
2026-07-23 22:46:10 +08:00
llmProviderKeyId : service . llmProviderKeyId || '' ,
llmModel : service . llmModel || '' ,
2026-07-23 22:27:07 +08:00
timeoutMs : service . timeoutMs ,
} )
2026-07-23 21:38:36 +08:00
. then ( ( result ) => response ( id , { content : [ { type : 'text' , text : JSON . stringify ( result ) } ] , structuredContent : result } ) )
. catch ( ( err ) => error ( id , SEARCH _ERROR _CODES . PROVIDER _UNAVAILABLE , err . message ) ) ;
}
2026-07-23 22:27:07 +08:00
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 ,
} )
2026-09-09 22:05:02 +08:00
. then ( async ( result ) => {
const rawText = result . report || JSON . stringify ( result ) ;
const kind = name === 'tkmind_research_status' ? 'tkmind_research_status' : name ;
const text = name === 'tkmind_research_status'
? await compactToolText ( rawText , kind )
: rawText ;
return response ( id , {
content : [ { type : 'text' , text } ] ,
structuredContent : result ,
} ) ;
} )
2026-07-23 22:27:07 +08:00
. catch ( ( err ) => error ( id , SEARCH _ERROR _CODES . PROVIDER _UNAVAILABLE , err . message ) ) ;
}
2026-07-15 13:46:17 +08:00
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' ) ; } } ) ;