From 5a5e7157680226542712a982d22bcf038598b429 Mon Sep 17 00:00:00 2001 From: Bobby Hyam Date: Fri, 13 Feb 2026 21:42:02 +0000 Subject: [PATCH 1/3] feat: improve agent ergonomics with better tool descriptions and disambiguation Rewrite tool descriptions to be action-first with clear use-case guidance, add server-level instructions, constrain role to enum, add idempotentHint, and improve output schema descriptions. Co-Authored-By: Claude Opus 4.6 --- src/server.ts | 61 +++++++++++++++++++++++++++++++++++---------------- 1 file changed, 42 insertions(+), 19 deletions(-) diff --git a/src/server.ts b/src/server.ts index a2a6f1f..ff2285a 100644 --- a/src/server.ts +++ b/src/server.ts @@ -247,13 +247,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. " + + "Use perplexity_research for in-depth multi-source investigation (slow, 30s+). " + + "Use perplexity_reason for complex analysis requiring step-by-step logic. " + + "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"), }); @@ -263,7 +274,7 @@ export function createPerplexityServer(serviceOrigin?: string) { .describe("If true, removes ... tags and their content from the response to save context tokens. Default is false."); const responseOutputSchema = { - response: z.string().describe("The response from Perplexity"), + response: z.string().describe("AI-generated text response with numbered citation references"), }; // Input schemas @@ -274,14 +285,17 @@ export function createPerplexityServer(serviceOrigin?: string) { "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. " + + "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) => { @@ -299,14 +313,18 @@ 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.", + 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: messagesWithStripThinkingInputSchema as any, outputSchema: responseOutputSchema as any, annotations: { readOnlyHint: true, openWorldHint: true, + idempotentHint: true, }, }, async (args: any) => { @@ -325,14 +343,17 @@ 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. " + + "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) => { @@ -358,21 +379,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) => { From 69c73e1aafdc21c5f3c60cd390aec1a2c16571c3 Mon Sep 17 00:00:00 2001 From: Kesku Date: Mon, 16 Feb 2026 00:09:13 +0000 Subject: [PATCH 2/3] feat: add ChatCompletionOptions and thread API parameters --- src/server.ts | 10 ++++++++-- src/types.ts | 7 +++++++ 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/src/server.ts b/src/server.ts index ff2285a..9b8b6b9 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(); 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; From 9cec29affe4f3b3ebfb19e6b7397cb31eb524040 Mon Sep 17 00:00:00 2001 From: Kesku Date: Mon, 16 Feb 2026 00:14:14 +0000 Subject: [PATCH 3/3] new input options and improved tool descriptions --- src/server.ts | 82 +++++++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 70 insertions(+), 12 deletions(-) diff --git a/src/server.ts b/src/server.ts index 9b8b6b9..2d6daa5 100644 --- a/src/server.ts +++ b/src/server.ts @@ -262,9 +262,9 @@ export function createPerplexityServer(serviceOrigin?: string) { 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. " + - "Use perplexity_research for in-depth multi-source investigation (slow, 30s+). " + - "Use perplexity_reason for complex analysis requiring step-by-step logic. " + + "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.", } ); @@ -279,13 +279,41 @@ 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("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", @@ -294,6 +322,7 @@ export function createPerplexityServer(serviceOrigin?: string) { 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, @@ -305,9 +334,19 @@ export function createPerplexityServer(serviceOrigin?: string) { }, }, 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 }, @@ -325,7 +364,7 @@ export function createPerplexityServer(serviceOrigin?: string) { "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: messagesWithStripThinkingInputSchema as any, + inputSchema: researchInputSchema as any, outputSchema: responseOutputSchema as any, annotations: { readOnlyHint: true, @@ -334,10 +373,17 @@ export function createPerplexityServer(serviceOrigin?: string) { }, }, 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 }, @@ -352,6 +398,7 @@ export function createPerplexityServer(serviceOrigin?: string) { 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, @@ -363,10 +410,21 @@ export function createPerplexityServer(serviceOrigin?: string) { }, }, 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 },