UNPKG

noodle-perplexity-mcp

Version:

A Perplexity API Model Context Protocol (MCP) server that unlocks Perplexity's search-augmented AI capabilities for LLM agents. Features robust error handling, secure input validation, and transparent reasoning with the showThinking parameter. Built with

115 lines (114 loc) 6.22 kB
/** * @fileoverview Handles registration and error handling for the `perplexity_deep_research` tool. * @module src/mcp-server/tools/perplexityDeepResearch/registration */ import { ErrorHandler, logger, requestContextService } from "../../../utils/index.js"; import { PerplexityDeepResearchInputSchema, perplexityDeepResearchLogic, PerplexityDeepResearchResponseSchema, } from "./logic.js"; /** * Registers the 'perplexity_deep_research' tool with the MCP server instance. * @param server - The MCP server instance. */ export const registerPerplexityDeepResearchTool = async (server) => { const toolName = "perplexity_deep_research"; const toolDescription = "[HIGH-COST - REQUIRES EXPLICIT USER REQUEST] Exhaustive multi-source research producing 10,000+ word professional reports (cost: $0.40-$2+ per query, 10x more expensive than perplexity_ask). **DO NOT use automatically or infer need - ONLY when user EXPLICITLY says 'deep research', 'comprehensive research report', 'exhaustive investigation', or specifically requests perplexity deep research.** For regular research questions, always use perplexity_ask instead. Use reasoning_effort parameter to control cost/depth. (Ex. User explicitly says: 'I need a deep research report on the quantum computing industry' or 'Use perplexity deep research to analyze...')"; server.registerTool(toolName, { title: "Perplexity Deep Research", description: toolDescription, inputSchema: PerplexityDeepResearchInputSchema.shape, outputSchema: PerplexityDeepResearchResponseSchema.shape, annotations: { readOnlyHint: false, openWorldHint: true, }, }, async (params) => { const handlerContext = requestContextService.createRequestContext({ toolName, }); try { const result = await perplexityDeepResearchLogic(params, handlerContext); // --- Parse <think> block --- // Deep research responses typically include extensive <think> blocks const thinkRegex = /^\s*<think>(.*?)<\/think>\s*(.*)$/s; const match = result.rawResultText.match(thinkRegex); let thinkingContent = null; let mainContent; if (match) { thinkingContent = match[1].trim(); mainContent = match[2].trim(); } else { mainContent = result.rawResultText.trim(); } // --- Construct Final Response --- // Include thinking blocks to show research strategy and reasoning let responseText = ''; if (thinkingContent) { responseText += `## Research Strategy & Reasoning\n\n<think>\n${thinkingContent}\n</think>\n\n---\n\n`; } responseText += mainContent; // Add sources/citations section if (result.citations && result.citations.length > 0) { responseText += `\n\n## Citations\n\n`; result.citations.forEach((url, i) => { responseText += `[${i + 1}] ${url}\n`; }); } // Add search results with snippets if available if (result.searchResults && result.searchResults.length > 0) { responseText += `\n\n## Search Results Used (${result.searchResults.length} sources)\n\n`; result.searchResults.slice(0, 10).forEach((sr, i) => { responseText += `[${i + 1}] **${sr.title}**\n`; responseText += ` URL: ${sr.url}\n`; if (sr.snippet) { responseText += ` Snippet: ${sr.snippet.substring(0, 200)}${sr.snippet.length > 200 ? '...' : ''}\n`; } if (sr.date) { responseText += ` Date: ${sr.date}\n`; } responseText += `\n`; }); if (result.searchResults.length > 10) { responseText += `... and ${result.searchResults.length - 10} more sources\n`; } } // Add cost breakdown if available if (result.costBreakdown) { responseText += `\n\n## Cost Breakdown\n\n`; responseText += `- Input tokens: $${result.costBreakdown.input_tokens_cost.toFixed(3)}\n`; responseText += `- Output tokens: $${result.costBreakdown.output_tokens_cost.toFixed(3)}\n`; responseText += `- Citation tokens: $${result.costBreakdown.citation_tokens_cost.toFixed(3)}\n`; responseText += `- Reasoning tokens: $${result.costBreakdown.reasoning_tokens_cost.toFixed(3)}\n`; responseText += `- Search queries: $${result.costBreakdown.search_queries_cost.toFixed(3)}\n`; responseText += `- **Total: $${result.costBreakdown.total_cost.toFixed(3)}**\n`; } // Add usage stats if (result.usage) { responseText += `\n\n## Usage Statistics\n\n`; responseText += `- Reasoning tokens: ${result.usage.reasoning_tokens?.toLocaleString() || 'N/A'}\n`; responseText += `- Search queries performed: ${result.usage.num_search_queries || 'N/A'}\n`; responseText += `- Total tokens: ${result.usage.total_tokens.toLocaleString()}\n`; } return { structuredContent: result, content: [{ type: "text", text: responseText }], }; } catch (error) { const mcpError = ErrorHandler.handleError(error, { operation: toolName, context: handlerContext, input: params, }); return { isError: true, content: [{ type: "text", text: mcpError.message }], structuredContent: { code: mcpError.code, message: mcpError.message, details: mcpError.details, }, }; } }); logger.info(`Tool '${toolName}' registered successfully.`); };