Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
147 changes: 117 additions & 30 deletions src/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { fetch as undiciFetch, ProxyAgent } from "undici";
import type {
Message,
ChatCompletionResponse,
ChatCompletionOptions,
SearchResponse,
SearchRequestBody,
UndiciRequestOptions
Expand Down Expand Up @@ -63,7 +64,8 @@ export async function performChatCompletion(
messages: Message[],
model: string = "sonar-pro",
stripThinking: boolean = false,
serviceOrigin?: string
serviceOrigin?: string,
options?: ChatCompletionOptions
): Promise<string> {
if (!PERPLEXITY_API_KEY) {
throw new Error("PERPLEXITY_API_KEY environment variable is required");
Expand All @@ -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<string, unknown> = {
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();
Expand Down Expand Up @@ -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"),
});

Expand All @@ -262,32 +279,74 @@ export function createPerplexityServer(serviceOrigin?: string) {
const stripThinkingField = z.boolean().optional()
.describe("If true, removes <think>...</think> 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 },
Expand All @@ -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 },
Expand All @@ -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 },
Expand All @@ -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) => {
Expand Down
7 changes: 7 additions & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down