@perplexity-ai/mcp-server 0.6.2 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -6,14 +6,14 @@
6
6
  },
7
7
  "metadata": {
8
8
  "description": "Official Perplexity AI plugin providing real-time web search, reasoning, and research capabilities",
9
- "version": "0.6.2"
9
+ "version": "0.8.0"
10
10
  },
11
11
  "plugins": [
12
12
  {
13
13
  "name": "perplexity",
14
14
  "source": "./",
15
15
  "description": "Real-time web search, reasoning, and research through Perplexity's API",
16
- "version": "0.6.2",
16
+ "version": "0.8.0",
17
17
  "author": {
18
18
  "name": "Perplexity AI",
19
19
  "email": "api@perplexity.ai"
package/dist/server.js CHANGED
@@ -43,16 +43,89 @@ export function validateMessages(messages, toolName) {
43
43
  export function stripThinkingTokens(content) {
44
44
  return content.replace(/<think>[\s\S]*?<\/think>/g, '').trim();
45
45
  }
46
- export async function performChatCompletion(messages, model = "sonar-pro", stripThinking = false, serviceOrigin) {
46
+ export async function consumeSSEStream(response) {
47
+ const body = response.body;
48
+ if (!body) {
49
+ throw new Error("Response body is null");
50
+ }
51
+ const reader = body.getReader();
52
+ const decoder = new TextDecoder();
53
+ let contentParts = [];
54
+ let citations;
55
+ let usage;
56
+ let id;
57
+ let model;
58
+ let created;
59
+ let buffer = "";
60
+ while (true) {
61
+ const { done, value } = await reader.read();
62
+ if (done)
63
+ break;
64
+ buffer += decoder.decode(value, { stream: true });
65
+ const lines = buffer.split("\n");
66
+ // Keep the last potentially incomplete line in the buffer
67
+ buffer = lines.pop() || "";
68
+ for (const line of lines) {
69
+ const trimmed = line.trim();
70
+ if (!trimmed || !trimmed.startsWith("data:"))
71
+ continue;
72
+ const data = trimmed.slice("data:".length).trim();
73
+ if (data === "[DONE]")
74
+ continue;
75
+ try {
76
+ const parsed = JSON.parse(data);
77
+ if (parsed.id)
78
+ id = parsed.id;
79
+ if (parsed.model)
80
+ model = parsed.model;
81
+ if (parsed.created)
82
+ created = parsed.created;
83
+ if (parsed.citations)
84
+ citations = parsed.citations;
85
+ if (parsed.usage)
86
+ usage = parsed.usage;
87
+ const delta = parsed.choices?.[0]?.delta;
88
+ if (delta?.content) {
89
+ contentParts.push(delta.content);
90
+ }
91
+ }
92
+ catch {
93
+ // Skip malformed JSON chunks (e.g. keep-alive pings)
94
+ }
95
+ }
96
+ }
97
+ const assembled = {
98
+ choices: [
99
+ {
100
+ message: { content: contentParts.join("") },
101
+ finish_reason: "stop",
102
+ index: 0,
103
+ },
104
+ ],
105
+ ...(citations && { citations }),
106
+ ...(usage && { usage }),
107
+ ...(id && { id }),
108
+ ...(model && { model }),
109
+ ...(created && { created }),
110
+ };
111
+ return ChatCompletionResponseSchema.parse(assembled);
112
+ }
113
+ export async function performChatCompletion(messages, model = "sonar-pro", stripThinking = false, serviceOrigin, options) {
47
114
  if (!PERPLEXITY_API_KEY) {
48
115
  throw new Error("PERPLEXITY_API_KEY environment variable is required");
49
116
  }
50
117
  // Read timeout fresh each time to respect env var changes
51
118
  const TIMEOUT_MS = parseInt(process.env.PERPLEXITY_TIMEOUT_MS || "300000", 10);
119
+ const useStreaming = model === "sonar-deep-research";
52
120
  const url = new URL(`${PERPLEXITY_BASE_URL}/chat/completions`);
53
121
  const body = {
54
122
  model: model,
55
123
  messages: messages,
124
+ ...(useStreaming && { stream: true }),
125
+ ...(options?.search_recency_filter && { search_recency_filter: options.search_recency_filter }),
126
+ ...(options?.search_domain_filter && { search_domain_filter: options.search_domain_filter }),
127
+ ...(options?.search_context_size && { web_search_options: { search_context_size: options.search_context_size } }),
128
+ ...(options?.reasoning_effort && { reasoning_effort: options.reasoning_effort }),
56
129
  };
57
130
  const controller = new AbortController();
58
131
  const timeoutId = setTimeout(() => controller.abort(), TIMEOUT_MS);
@@ -92,8 +165,13 @@ export async function performChatCompletion(messages, model = "sonar-pro", strip
92
165
  }
93
166
  let data;
94
167
  try {
95
- const json = await response.json();
96
- data = ChatCompletionResponseSchema.parse(json);
168
+ if (useStreaming) {
169
+ data = await consumeSSEStream(response);
170
+ }
171
+ else {
172
+ const json = await response.json();
173
+ data = ChatCompletionResponseSchema.parse(json);
174
+ }
97
175
  }
98
176
  catch (error) {
99
177
  if (error instanceof z.ZodError) {
@@ -200,36 +278,76 @@ export async function performSearch(query, maxResults = 10, maxTokensPerPage = 1
200
278
  export function createPerplexityServer(serviceOrigin) {
201
279
  const server = new McpServer({
202
280
  name: "io.github.perplexityai/mcp-server",
203
- version: "0.6.2",
281
+ version: "0.8.0",
282
+ }, {
283
+ instructions: "Perplexity AI server for web-grounded search, research, and reasoning. " +
284
+ "Use perplexity_search for finding URLs, facts, and recent news. " +
285
+ "Use perplexity_ask for quick AI-answered questions with citations. Supports recency filters, domain restrictions, and search context size control. " +
286
+ "Use perplexity_research for in-depth multi-source investigation (slow, 30s+). Supports reasoning_effort parameter to control depth. " +
287
+ "Use perplexity_reason for complex analysis requiring step-by-step logic. Supports recency filters, domain restrictions, and search context size control. " +
288
+ "All tools are read-only and access live web data.",
204
289
  });
205
290
  const messageSchema = z.object({
206
- role: z.string().describe("Role of the message (e.g., system, user, assistant)"),
291
+ role: z.enum(["system", "user", "assistant"]).describe("Role of the message sender"),
207
292
  content: z.string().describe("The content of the message"),
208
293
  });
209
294
  const messagesField = z.array(messageSchema).describe("Array of conversation messages");
210
295
  const stripThinkingField = z.boolean().optional()
211
296
  .describe("If true, removes <think>...</think> tags and their content from the response to save context tokens. Default is false.");
297
+ const searchRecencyFilterField = z.enum(["hour", "day", "week", "month", "year"]).optional()
298
+ .describe("Filter search results by recency. Use 'hour' for very recent news, 'day' for today's updates, 'week' for this week, etc.");
299
+ const searchDomainFilterField = z.array(z.string()).optional()
300
+ .describe("Restrict search results to specific domains (e.g., ['wikipedia.org', 'arxiv.org']). Use '-' prefix for exclusion (e.g., ['-reddit.com']).");
301
+ const searchContextSizeField = z.enum(["low", "medium", "high"]).optional()
302
+ .describe("Controls how much web context is retrieved. 'low' (default) is fastest, 'high' provides more comprehensive results.");
303
+ const reasoningEffortField = z.enum(["minimal", "low", "medium", "high"]).optional()
304
+ .describe("Controls depth of deep research reasoning. Higher values produce more thorough analysis.");
212
305
  const responseOutputSchema = {
213
- response: z.string().describe("The response from Perplexity"),
306
+ response: z.string().describe("AI-generated text response with numbered citation references"),
214
307
  };
215
308
  // Input schemas
216
- const messagesOnlyInputSchema = { messages: messagesField };
217
- const messagesWithStripThinkingInputSchema = { messages: messagesField, strip_thinking: stripThinkingField };
309
+ const messagesOnlyInputSchema = {
310
+ messages: messagesField,
311
+ search_recency_filter: searchRecencyFilterField,
312
+ search_domain_filter: searchDomainFilterField,
313
+ search_context_size: searchContextSizeField,
314
+ };
315
+ const messagesWithStripThinkingInputSchema = {
316
+ messages: messagesField,
317
+ strip_thinking: stripThinkingField,
318
+ search_recency_filter: searchRecencyFilterField,
319
+ search_domain_filter: searchDomainFilterField,
320
+ search_context_size: searchContextSizeField,
321
+ };
322
+ const researchInputSchema = {
323
+ messages: messagesField,
324
+ strip_thinking: stripThinkingField,
325
+ reasoning_effort: reasoningEffortField,
326
+ };
218
327
  server.registerTool("perplexity_ask", {
219
328
  title: "Ask Perplexity",
220
- description: "Engages in a conversation using the Sonar API. " +
221
- "Accepts an array of messages (each with a role and content) " +
222
- "and returns a chat completion response from the Perplexity model.",
329
+ description: "Answer a question using web-grounded AI (Sonar Pro model). " +
330
+ "Best for: quick factual questions, summaries, explanations, and general Q&A. " +
331
+ "Returns a text response with numbered citations. Fastest and cheapest option. " +
332
+ "Supports filtering by recency (hour/day/week/month/year), domain restrictions, and search context size. " +
333
+ "For in-depth multi-source research, use perplexity_research instead. " +
334
+ "For step-by-step reasoning and analysis, use perplexity_reason instead.",
223
335
  inputSchema: messagesOnlyInputSchema,
224
336
  outputSchema: responseOutputSchema,
225
337
  annotations: {
226
338
  readOnlyHint: true,
227
339
  openWorldHint: true,
340
+ idempotentHint: true,
228
341
  },
229
342
  }, async (args) => {
230
- const { messages } = args;
343
+ const { messages, search_recency_filter, search_domain_filter, search_context_size } = args;
231
344
  validateMessages(messages, "perplexity_ask");
232
- const result = await performChatCompletion(messages, "sonar-pro", false, serviceOrigin);
345
+ const options = {
346
+ ...(search_recency_filter && { search_recency_filter }),
347
+ ...(search_domain_filter && { search_domain_filter }),
348
+ ...(search_context_size && { search_context_size }),
349
+ };
350
+ const result = await performChatCompletion(messages, "sonar-pro", false, serviceOrigin, Object.keys(options).length > 0 ? options : undefined);
233
351
  return {
234
352
  content: [{ type: "text", text: result }],
235
353
  structuredContent: { response: result },
@@ -237,20 +355,27 @@ export function createPerplexityServer(serviceOrigin) {
237
355
  });
238
356
  server.registerTool("perplexity_research", {
239
357
  title: "Deep Research",
240
- description: "Performs deep research using the Perplexity API. " +
241
- "Accepts an array of messages (each with a role and content) " +
242
- "and returns a comprehensive research response with citations.",
243
- inputSchema: messagesWithStripThinkingInputSchema,
358
+ description: "Conduct deep, multi-source research on a topic (Sonar Deep Research model). " +
359
+ "Best for: literature reviews, comprehensive overviews, investigative queries needing " +
360
+ "many sources. Returns a detailed response with numbered citations. " +
361
+ "Significantly slower than other tools (30+ seconds). " +
362
+ "For quick factual questions, use perplexity_ask instead. " +
363
+ "For logical analysis and reasoning, use perplexity_reason instead.",
364
+ inputSchema: researchInputSchema,
244
365
  outputSchema: responseOutputSchema,
245
366
  annotations: {
246
367
  readOnlyHint: true,
247
368
  openWorldHint: true,
369
+ idempotentHint: true,
248
370
  },
249
371
  }, async (args) => {
250
- const { messages, strip_thinking } = args;
372
+ const { messages, strip_thinking, reasoning_effort } = args;
251
373
  validateMessages(messages, "perplexity_research");
252
374
  const stripThinking = typeof strip_thinking === "boolean" ? strip_thinking : false;
253
- const result = await performChatCompletion(messages, "sonar-deep-research", stripThinking, serviceOrigin);
375
+ const options = {
376
+ ...(reasoning_effort && { reasoning_effort }),
377
+ };
378
+ const result = await performChatCompletion(messages, "sonar-deep-research", stripThinking, serviceOrigin, Object.keys(options).length > 0 ? options : undefined);
254
379
  return {
255
380
  content: [{ type: "text", text: result }],
256
381
  structuredContent: { response: result },
@@ -258,20 +383,29 @@ export function createPerplexityServer(serviceOrigin) {
258
383
  });
259
384
  server.registerTool("perplexity_reason", {
260
385
  title: "Advanced Reasoning",
261
- description: "Performs reasoning tasks using the Perplexity API. " +
262
- "Accepts an array of messages (each with a role and content) " +
263
- "and returns a well-reasoned response using the sonar-reasoning-pro model.",
386
+ description: "Analyze a question using step-by-step reasoning with web grounding (Sonar Reasoning Pro model). " +
387
+ "Best for: math, logic, comparisons, complex arguments, and tasks requiring chain-of-thought. " +
388
+ "Returns a reasoned response with numbered citations. " +
389
+ "Supports filtering by recency (hour/day/week/month/year), domain restrictions, and search context size. " +
390
+ "For quick factual questions, use perplexity_ask instead. " +
391
+ "For comprehensive multi-source research, use perplexity_research instead.",
264
392
  inputSchema: messagesWithStripThinkingInputSchema,
265
393
  outputSchema: responseOutputSchema,
266
394
  annotations: {
267
395
  readOnlyHint: true,
268
396
  openWorldHint: true,
397
+ idempotentHint: true,
269
398
  },
270
399
  }, async (args) => {
271
- const { messages, strip_thinking } = args;
400
+ const { messages, strip_thinking, search_recency_filter, search_domain_filter, search_context_size } = args;
272
401
  validateMessages(messages, "perplexity_reason");
273
402
  const stripThinking = typeof strip_thinking === "boolean" ? strip_thinking : false;
274
- const result = await performChatCompletion(messages, "sonar-reasoning-pro", stripThinking, serviceOrigin);
403
+ const options = {
404
+ ...(search_recency_filter && { search_recency_filter }),
405
+ ...(search_domain_filter && { search_domain_filter }),
406
+ ...(search_context_size && { search_context_size }),
407
+ };
408
+ const result = await performChatCompletion(messages, "sonar-reasoning-pro", stripThinking, serviceOrigin, Object.keys(options).length > 0 ? options : undefined);
275
409
  return {
276
410
  content: [{ type: "text", text: result }],
277
411
  structuredContent: { response: result },
@@ -287,18 +421,20 @@ export function createPerplexityServer(serviceOrigin) {
287
421
  .describe("ISO 3166-1 alpha-2 country code for regional results (e.g., 'US', 'GB')"),
288
422
  };
289
423
  const searchOutputSchema = {
290
- results: z.string().describe("Formatted search results"),
424
+ results: z.string().describe("Formatted search results, each with title, URL, snippet, and date"),
291
425
  };
292
426
  server.registerTool("perplexity_search", {
293
427
  title: "Search the Web",
294
- description: "Performs web search using the Perplexity Search API. " +
295
- "Returns ranked search results with titles, URLs, snippets, and metadata. " +
296
- "Perfect for finding up-to-date facts, news, or specific information.",
428
+ description: "Search the web and return a ranked list of results with titles, URLs, snippets, and dates. " +
429
+ "Best for: finding specific URLs, checking recent news, verifying facts, discovering sources. " +
430
+ "Returns formatted results (title, URL, snippet, date) no AI synthesis. " +
431
+ "For AI-generated answers with citations, use perplexity_ask instead.",
297
432
  inputSchema: searchInputSchema,
298
433
  outputSchema: searchOutputSchema,
299
434
  annotations: {
300
435
  readOnlyHint: true,
301
436
  openWorldHint: true,
437
+ idempotentHint: true,
302
438
  },
303
439
  }, async (args) => {
304
440
  const { query, max_results, max_tokens_per_page, country } = args;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@perplexity-ai/mcp-server",
3
- "version": "0.6.2",
4
- "mcpName": "io.github.perplexityai/mcp-server",
3
+ "version": "0.8.0",
4
+ "mcpName": "ai.perplexityai/mcp-server",
5
5
  "description": "Real-time web search, reasoning, and research through Perplexity's API",
6
6
  "keywords": [
7
7
  "ai",