diff --git a/src/server.ts b/src/server.ts index a2a6f1f..2d6daa5 100644 --- a/src/server.ts +++ b/src/server.ts @@ -4,6 +4,7 @@ import { fetch as undiciFetch, ProxyAgent } from "undici"; import type { Message, ChatCompletionResponse, + ChatCompletionOptions, SearchResponse, SearchRequestBody, UndiciRequestOptions @@ -63,7 +64,8 @@ export async function performChatCompletion( messages: Message[], model: string = "sonar-pro", stripThinking: boolean = false, - serviceOrigin?: string + serviceOrigin?: string, + options?: ChatCompletionOptions ): Promise { if (!PERPLEXITY_API_KEY) { throw new Error("PERPLEXITY_API_KEY environment variable is required"); @@ -73,9 +75,13 @@ export async function performChatCompletion( const TIMEOUT_MS = parseInt(process.env.PERPLEXITY_TIMEOUT_MS || "300000", 10); const url = new URL(`${PERPLEXITY_BASE_URL}/chat/completions`); - const body = { + const body: Record = { model: model, messages: messages, + ...(options?.search_recency_filter && { search_recency_filter: options.search_recency_filter }), + ...(options?.search_domain_filter && { search_domain_filter: options.search_domain_filter }), + ...(options?.search_context_size && { web_search_options: { search_context_size: options.search_context_size } }), + ...(options?.reasoning_effort && { reasoning_effort: options.reasoning_effort }), }; const controller = new AbortController(); @@ -247,13 +253,24 @@ export async function performSearch( } export function createPerplexityServer(serviceOrigin?: string) { - const server = new McpServer({ - name: "io.github.perplexityai/mcp-server", - version: "0.6.2", - }); + const server = new McpServer( + { + name: "io.github.perplexityai/mcp-server", + version: "0.6.2", + }, + { + instructions: + "Perplexity AI server for web-grounded search, research, and reasoning. " + + "Use perplexity_search for finding URLs, facts, and recent news. " + + "Use perplexity_ask for quick AI-answered questions with citations. Supports recency filters, domain restrictions, and search context size control. " + + "Use perplexity_research for in-depth multi-source investigation (slow, 30s+). Supports reasoning_effort parameter to control depth. " + + "Use perplexity_reason for complex analysis requiring step-by-step logic. Supports recency filters, domain restrictions, and search context size control. " + + "All tools are read-only and access live web data.", + } + ); const messageSchema = z.object({ - role: z.string().describe("Role of the message (e.g., system, user, assistant)"), + role: z.enum(["system", "user", "assistant"]).describe("Role of the message sender"), content: z.string().describe("The content of the message"), }); @@ -262,32 +279,74 @@ export function createPerplexityServer(serviceOrigin?: string) { const stripThinkingField = z.boolean().optional() .describe("If true, removes ... tags and their content from the response to save context tokens. Default is false."); + const searchRecencyFilterField = z.enum(["hour", "day", "week", "month", "year"]).optional() + .describe("Filter search results by recency. Use 'hour' for very recent news, 'day' for today's updates, 'week' for this week, etc."); + + const searchDomainFilterField = z.array(z.string()).optional() + .describe("Restrict search results to specific domains (e.g., ['wikipedia.org', 'arxiv.org']). Use '-' prefix for exclusion (e.g., ['-reddit.com'])."); + + const searchContextSizeField = z.enum(["low", "medium", "high"]).optional() + .describe("Controls how much web context is retrieved. 'low' (default) is fastest, 'high' provides more comprehensive results."); + + const reasoningEffortField = z.enum(["minimal", "low", "medium", "high"]).optional() + .describe("Controls depth of deep research reasoning. Higher values produce more thorough analysis."); + const responseOutputSchema = { - response: z.string().describe("The response from Perplexity"), + response: z.string().describe("AI-generated text response with numbered citation references"), }; // Input schemas - const messagesOnlyInputSchema = { messages: messagesField }; - const messagesWithStripThinkingInputSchema = { messages: messagesField, strip_thinking: stripThinkingField }; + const messagesOnlyInputSchema = { + messages: messagesField, + search_recency_filter: searchRecencyFilterField, + search_domain_filter: searchDomainFilterField, + search_context_size: searchContextSizeField, + }; + const messagesWithStripThinkingInputSchema = { + messages: messagesField, + strip_thinking: stripThinkingField, + search_recency_filter: searchRecencyFilterField, + search_domain_filter: searchDomainFilterField, + search_context_size: searchContextSizeField, + }; + const researchInputSchema = { + messages: messagesField, + strip_thinking: stripThinkingField, + reasoning_effort: reasoningEffortField, + }; server.registerTool( "perplexity_ask", { title: "Ask Perplexity", - description: "Engages in a conversation using the Sonar API. " + - "Accepts an array of messages (each with a role and content) " + - "and returns a chat completion response from the Perplexity model.", + description: "Answer a question using web-grounded AI (Sonar Pro model). " + + "Best for: quick factual questions, summaries, explanations, and general Q&A. " + + "Returns a text response with numbered citations. Fastest and cheapest option. " + + "Supports filtering by recency (hour/day/week/month/year), domain restrictions, and search context size. " + + "For in-depth multi-source research, use perplexity_research instead. " + + "For step-by-step reasoning and analysis, use perplexity_reason instead.", inputSchema: messagesOnlyInputSchema as any, outputSchema: responseOutputSchema as any, annotations: { readOnlyHint: true, openWorldHint: true, + idempotentHint: true, }, }, async (args: any) => { - const { messages } = args as { messages: Message[] }; + const { messages, search_recency_filter, search_domain_filter, search_context_size } = args as { + messages: Message[]; + search_recency_filter?: "hour" | "day" | "week" | "month" | "year"; + search_domain_filter?: string[]; + search_context_size?: "low" | "medium" | "high"; + }; validateMessages(messages, "perplexity_ask"); - const result = await performChatCompletion(messages, "sonar-pro", false, serviceOrigin); + const options = { + ...(search_recency_filter && { search_recency_filter }), + ...(search_domain_filter && { search_domain_filter }), + ...(search_context_size && { search_context_size }), + }; + const result = await performChatCompletion(messages, "sonar-pro", false, serviceOrigin, Object.keys(options).length > 0 ? options : undefined); return { content: [{ type: "text" as const, text: result }], structuredContent: { response: result }, @@ -299,21 +358,32 @@ export function createPerplexityServer(serviceOrigin?: string) { "perplexity_research", { title: "Deep Research", - description: "Performs deep research using the Perplexity API. " + - "Accepts an array of messages (each with a role and content) " + - "and returns a comprehensive research response with citations.", - inputSchema: messagesWithStripThinkingInputSchema as any, + description: "Conduct deep, multi-source research on a topic (Sonar Deep Research model). " + + "Best for: literature reviews, comprehensive overviews, investigative queries needing " + + "many sources. Returns a detailed response with numbered citations. " + + "Significantly slower than other tools (30+ seconds). " + + "For quick factual questions, use perplexity_ask instead. " + + "For logical analysis and reasoning, use perplexity_reason instead.", + inputSchema: researchInputSchema as any, outputSchema: responseOutputSchema as any, annotations: { readOnlyHint: true, openWorldHint: true, + idempotentHint: true, }, }, async (args: any) => { - const { messages, strip_thinking } = args as { messages: Message[]; strip_thinking?: boolean }; + const { messages, strip_thinking, reasoning_effort } = args as { + messages: Message[]; + strip_thinking?: boolean; + reasoning_effort?: "minimal" | "low" | "medium" | "high"; + }; validateMessages(messages, "perplexity_research"); const stripThinking = typeof strip_thinking === "boolean" ? strip_thinking : false; - const result = await performChatCompletion(messages, "sonar-deep-research", stripThinking, serviceOrigin); + const options = { + ...(reasoning_effort && { reasoning_effort }), + }; + const result = await performChatCompletion(messages, "sonar-deep-research", stripThinking, serviceOrigin, Object.keys(options).length > 0 ? options : undefined); return { content: [{ type: "text" as const, text: result }], structuredContent: { response: result }, @@ -325,21 +395,36 @@ export function createPerplexityServer(serviceOrigin?: string) { "perplexity_reason", { title: "Advanced Reasoning", - description: "Performs reasoning tasks using the Perplexity API. " + - "Accepts an array of messages (each with a role and content) " + - "and returns a well-reasoned response using the sonar-reasoning-pro model.", + description: "Analyze a question using step-by-step reasoning with web grounding (Sonar Reasoning Pro model). " + + "Best for: math, logic, comparisons, complex arguments, and tasks requiring chain-of-thought. " + + "Returns a reasoned response with numbered citations. " + + "Supports filtering by recency (hour/day/week/month/year), domain restrictions, and search context size. " + + "For quick factual questions, use perplexity_ask instead. " + + "For comprehensive multi-source research, use perplexity_research instead.", inputSchema: messagesWithStripThinkingInputSchema as any, outputSchema: responseOutputSchema as any, annotations: { readOnlyHint: true, openWorldHint: true, + idempotentHint: true, }, }, async (args: any) => { - const { messages, strip_thinking } = args as { messages: Message[]; strip_thinking?: boolean }; + const { messages, strip_thinking, search_recency_filter, search_domain_filter, search_context_size } = args as { + messages: Message[]; + strip_thinking?: boolean; + search_recency_filter?: "hour" | "day" | "week" | "month" | "year"; + search_domain_filter?: string[]; + search_context_size?: "low" | "medium" | "high"; + }; validateMessages(messages, "perplexity_reason"); const stripThinking = typeof strip_thinking === "boolean" ? strip_thinking : false; - const result = await performChatCompletion(messages, "sonar-reasoning-pro", stripThinking, serviceOrigin); + const options = { + ...(search_recency_filter && { search_recency_filter }), + ...(search_domain_filter && { search_domain_filter }), + ...(search_context_size && { search_context_size }), + }; + const result = await performChatCompletion(messages, "sonar-reasoning-pro", stripThinking, serviceOrigin, Object.keys(options).length > 0 ? options : undefined); return { content: [{ type: "text" as const, text: result }], structuredContent: { response: result }, @@ -358,21 +443,23 @@ export function createPerplexityServer(serviceOrigin?: string) { }; const searchOutputSchema = { - results: z.string().describe("Formatted search results"), + results: z.string().describe("Formatted search results, each with title, URL, snippet, and date"), }; server.registerTool( "perplexity_search", { title: "Search the Web", - description: "Performs web search using the Perplexity Search API. " + - "Returns ranked search results with titles, URLs, snippets, and metadata. " + - "Perfect for finding up-to-date facts, news, or specific information.", + description: "Search the web and return a ranked list of results with titles, URLs, snippets, and dates. " + + "Best for: finding specific URLs, checking recent news, verifying facts, discovering sources. " + + "Returns formatted results (title, URL, snippet, date) — no AI synthesis. " + + "For AI-generated answers with citations, use perplexity_ask instead.", inputSchema: searchInputSchema as any, outputSchema: searchOutputSchema as any, annotations: { readOnlyHint: true, openWorldHint: true, + idempotentHint: true, }, }, async (args: any) => { diff --git a/src/types.ts b/src/types.ts index 2721242..f7d6fb9 100644 --- a/src/types.ts +++ b/src/types.ts @@ -56,6 +56,13 @@ export interface SearchRequestBody { country?: string; } +export interface ChatCompletionOptions { + search_recency_filter?: "hour" | "day" | "week" | "month" | "year"; + search_domain_filter?: string[]; + search_context_size?: "low" | "medium" | "high"; + reasoning_effort?: "minimal" | "low" | "medium" | "high"; +} + export interface UndiciRequestOptions { [key: string]: unknown; dispatcher?: ProxyAgent;