Skip to main content
Glama

Kagi MCP

by yuki-yano
index.ts7.04 kB
#!/usr/bin/env node import { Server } from '@modelcontextprotocol/sdk/server/index.js'; import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'; import { CallToolRequestSchema, CallToolResult, ErrorCode, ListToolsRequestSchema, ListToolsResult, McpError, Tool, } from '@modelcontextprotocol/sdk/types.js'; import { KagiClient, SearchResponse } from './kagi-client.js'; const kagiClient = new KagiClient(); const server = new Server({ name: 'kagi-mcp', version: '1.0.0', }, { capabilities: { tools: {}, }, }); const SEARCH_TOOL: Tool = { name: 'kagi_search_fetch', description: 'Fetch web results based on one or more queries using the Kagi Search API. Use for general search and when the user explicitly tells you to \'fetch\' results/information. Results are from all queries given. They are numbered continuously, so that a user may be able to refer to a result by a specific number.', inputSchema: { type: 'object', properties: { queries: { type: 'array', items: { type: 'string', }, description: 'One or more concise, keyword-focused search queries. Include essential context within each query for standalone use.', }, }, required: ['queries'], }, }; const SUMMARIZER_TOOL: Tool = { name: 'kagi_summarizer', description: 'Summarize content from a URL using the Kagi Summarizer API. The Summarizer can summarize any document type (text webpage, video, audio, etc.)', inputSchema: { type: 'object', properties: { url: { type: 'string', description: 'A URL to a document to summarize.', }, summary_type: { type: 'string', enum: ['summary', 'takeaway'], default: 'summary', description: 'Type of summary to produce. Options are \'summary\' for paragraph prose and \'takeaway\' for a bulleted list of key points.', }, target_language: { type: 'string', description: 'Desired output language using language codes (e.g., \'EN\' for English). If not specified, the document\'s original language influences the output.', }, }, required: ['url'], }, }; server.setRequestHandler(ListToolsRequestSchema, async (): Promise<ListToolsResult> => { return { tools: [SEARCH_TOOL, SUMMARIZER_TOOL], }; }); function formatSearchResults(queries: string[], responses: SearchResponse[]): string { const resultTemplate = (result: { result_number: number; title: string; url: string; published: string; snippet: string; }) => `${result.result_number}: ${result.title} ${result.url} Published Date: ${result.published} ${result.snippet}`; const queryResponseTemplate = (query: string, formattedResults: string) => `----- Results for search query "${query}": ----- ${formattedResults}`; const perQueryResponseStrs: string[] = []; let startIndex = 1; for (let i = 0; i < queries.length; i++) { const query = queries[i]; const response = responses[i]; // t == 0 is search result, t == 1 is related searches const results = response.data.filter(result => result.t === 0); const formattedResultsList = results.map((result, idx) => resultTemplate({ result_number: startIndex + idx, title: result.title, url: result.url, published: result.published || 'Not Available', snippet: result.snippet, }) ); startIndex += results.length; const formattedResultsStr = formattedResultsList.join('\n\n'); const queryResponseStr = queryResponseTemplate(query, formattedResultsStr); perQueryResponseStrs.push(queryResponseStr); } return perQueryResponseStrs.join('\n\n'); } server.setRequestHandler(CallToolRequestSchema, async (request): Promise<CallToolResult> => { try { const { name, arguments: args } = request.params; if (name === 'kagi_search_fetch') { const queries = args?.queries as string[] | undefined; if (!queries || queries.length === 0) { throw new McpError( ErrorCode.InvalidParams, 'Search called with no queries.' ); } try { // Execute searches in parallel const searchPromises = queries.map(query => kagiClient.search(query)); const results = await Promise.all(searchPromises); const formattedResults = formatSearchResults(queries, results); return { content: [ { type: 'text', text: formattedResults, }, ], }; } catch (error) { return { content: [ { type: 'text', text: `Error: ${error instanceof Error ? error.message : String(error)}`, }, ], }; } } if (name === 'kagi_summarizer') { const url = args?.url as string | undefined; if (!url) { throw new McpError( ErrorCode.InvalidParams, 'Summarizer called with no URL.' ); } const summaryType = args?.summary_type as 'summary' | 'takeaway' | undefined; const targetLanguage = args?.target_language as string | undefined; const engine = process.env.KAGI_SUMMARIZER_ENGINE || 'cecil'; const validEngines = ['cecil', 'agnes', 'daphne', 'muriel']; if (!validEngines.includes(engine)) { throw new McpError( ErrorCode.InvalidParams, `Summarizer configured incorrectly, invalid summarization engine set: ${engine}. Must be one of the following: ${validEngines.join(', ')}` ); } try { const summary = await kagiClient.summarize({ url, engine: engine as 'cecil' | 'agnes' | 'daphne' | 'muriel', summary_type: summaryType, target_language: targetLanguage, }); return { content: [ { type: 'text', text: summary.data.output, }, ], }; } catch (error) { return { content: [ { type: 'text', text: `Error: ${error instanceof Error ? error.message : String(error)}`, }, ], }; } } throw new McpError( ErrorCode.MethodNotFound, `Unknown tool: ${name}` ); } catch (error) { if (error instanceof McpError) { throw error; } throw new McpError( ErrorCode.InternalError, `Unexpected error: ${error instanceof Error ? error.message : String(error)}` ); } }); async function main() { const transport = new StdioServerTransport(); await server.connect(transport); // Handle graceful shutdown process.on('SIGINT', async () => { await server.close(); process.exit(0); }); } main().catch((error) => { console.error('Fatal error:', error); process.exit(1); });

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/yuki-yano/kagi-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server