@perplexity-ai/mcp-server 0.4.1 → 0.5.1

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.
package/dist/index.js CHANGED
@@ -1,565 +1,29 @@
1
1
  #!/usr/bin/env node
2
- var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
3
- function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
4
- return new (P || (P = Promise))(function (resolve, reject) {
5
- function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
6
- function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
7
- function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
8
- step((generator = generator.apply(thisArg, _arguments || [])).next());
9
- });
10
- };
11
- import { Server } from "@modelcontextprotocol/sdk/server/index.js";
12
2
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
13
- import { CallToolRequestSchema, ListToolsRequestSchema, } from "@modelcontextprotocol/sdk/types.js";
14
- import { fetch as undiciFetch, ProxyAgent } from "undici";
15
- /**
16
- * Definition of the Perplexity Ask Tool.
17
- * This tool accepts an array of messages and returns a chat completion response
18
- * from the Perplexity API, with citations appended to the message if provided.
19
- */
20
- const PERPLEXITY_ASK_TOOL = {
21
- name: "perplexity_ask",
22
- title: "Ask Perplexity",
23
- description: "Engages in a conversation using the Sonar API. " +
24
- "Accepts an array of messages (each with a role and content) " +
25
- "and returns a ask completion response from the Perplexity model.",
26
- inputSchema: {
27
- type: "object",
28
- properties: {
29
- messages: {
30
- type: "array",
31
- items: {
32
- type: "object",
33
- properties: {
34
- role: {
35
- type: "string",
36
- description: "Role of the message (e.g., system, user, assistant)",
37
- },
38
- content: {
39
- type: "string",
40
- description: "The content of the message",
41
- },
42
- },
43
- required: ["role", "content"],
44
- },
45
- description: "Array of conversation messages",
46
- },
47
- },
48
- required: ["messages"],
49
- },
50
- annotations: {
51
- readOnlyHint: true,
52
- openWorldHint: true,
53
- },
54
- };
55
- /**
56
- * Definition of the Perplexity Research Tool.
57
- * This tool performs deep research queries using the Perplexity API.
58
- */
59
- const PERPLEXITY_RESEARCH_TOOL = {
60
- name: "perplexity_research",
61
- title: "Deep Research",
62
- description: "Performs deep research using the Perplexity API. " +
63
- "Accepts an array of messages (each with a role and content) " +
64
- "and returns a comprehensive research response with citations.",
65
- inputSchema: {
66
- type: "object",
67
- properties: {
68
- messages: {
69
- type: "array",
70
- items: {
71
- type: "object",
72
- properties: {
73
- role: {
74
- type: "string",
75
- description: "Role of the message (e.g., system, user, assistant)",
76
- },
77
- content: {
78
- type: "string",
79
- description: "The content of the message",
80
- },
81
- },
82
- required: ["role", "content"],
83
- },
84
- description: "Array of conversation messages",
85
- },
86
- strip_thinking: {
87
- type: "boolean",
88
- description: "If true, removes <think>...</think> tags and their content from the response to save context tokens. Default is false.",
89
- },
90
- },
91
- required: ["messages"],
92
- },
93
- annotations: {
94
- readOnlyHint: true,
95
- openWorldHint: true,
96
- },
97
- };
98
- /**
99
- * Definition of the Perplexity Reason Tool.
100
- * This tool performs reasoning queries using the Perplexity API.
101
- */
102
- const PERPLEXITY_REASON_TOOL = {
103
- name: "perplexity_reason",
104
- title: "Advanced Reasoning",
105
- description: "Performs reasoning tasks using the Perplexity API. " +
106
- "Accepts an array of messages (each with a role and content) " +
107
- "and returns a well-reasoned response using the sonar-reasoning-pro model.",
108
- inputSchema: {
109
- type: "object",
110
- properties: {
111
- messages: {
112
- type: "array",
113
- items: {
114
- type: "object",
115
- properties: {
116
- role: {
117
- type: "string",
118
- description: "Role of the message (e.g., system, user, assistant)",
119
- },
120
- content: {
121
- type: "string",
122
- description: "The content of the message",
123
- },
124
- },
125
- required: ["role", "content"],
126
- },
127
- description: "Array of conversation messages",
128
- },
129
- strip_thinking: {
130
- type: "boolean",
131
- description: "If true, removes <think>...</think> tags and their content from the response to save context tokens. Default is false.",
132
- },
133
- },
134
- required: ["messages"],
135
- },
136
- annotations: {
137
- readOnlyHint: true,
138
- openWorldHint: true,
139
- },
140
- };
141
- /**
142
- * Definition of the Perplexity Search Tool.
143
- * This tool performs web search using the Perplexity Search API.
144
- */
145
- const PERPLEXITY_SEARCH_TOOL = {
146
- name: "perplexity_search",
147
- title: "Search the Web",
148
- description: "Performs web search using the Perplexity Search API. " +
149
- "Returns ranked search results with titles, URLs, snippets, and metadata. " +
150
- "Perfect for finding up-to-date facts, news, or specific information.",
151
- inputSchema: {
152
- type: "object",
153
- properties: {
154
- query: {
155
- type: "string",
156
- description: "Search query string",
157
- },
158
- max_results: {
159
- type: "number",
160
- description: "Maximum number of results to return (1-20, default: 10)",
161
- minimum: 1,
162
- maximum: 20,
163
- },
164
- max_tokens_per_page: {
165
- type: "number",
166
- description: "Maximum tokens to extract per webpage (default: 1024)",
167
- minimum: 256,
168
- maximum: 2048,
169
- },
170
- country: {
171
- type: "string",
172
- description: "ISO 3166-1 alpha-2 country code for regional results (e.g., 'US', 'GB')",
173
- },
174
- },
175
- required: ["query"],
176
- },
177
- annotations: {
178
- readOnlyHint: true,
179
- openWorldHint: true,
180
- },
181
- };
182
- // Retrieve the Perplexity API key from environment variables
3
+ import { createPerplexityServer } from "./server.js";
4
+ // Check for required API key
183
5
  const PERPLEXITY_API_KEY = process.env.PERPLEXITY_API_KEY;
184
6
  if (!PERPLEXITY_API_KEY) {
185
7
  console.error("Error: PERPLEXITY_API_KEY environment variable is required");
186
8
  process.exit(1);
187
9
  }
188
10
  /**
189
- * Gets the proxy URL from environment variables.
190
- * Checks PERPLEXITY_PROXY, HTTPS_PROXY, HTTP_PROXY in order.
191
- *
192
- * @returns {string | undefined} The proxy URL if configured, undefined otherwise
193
- */
194
- function getProxyUrl() {
195
- return process.env.PERPLEXITY_PROXY ||
196
- process.env.HTTPS_PROXY ||
197
- process.env.HTTP_PROXY ||
198
- undefined;
199
- }
200
- /**
201
- * Creates a proxy-aware fetch function.
202
- * Uses undici with ProxyAgent when a proxy is configured, otherwise uses native fetch.
203
- *
204
- * @param {string} url - The URL to fetch
205
- * @param {RequestInit} options - Fetch options
206
- * @returns {Promise<Response>} The fetch response
207
- */
208
- function proxyAwareFetch(url_1) {
209
- return __awaiter(this, arguments, void 0, function* (url, options = {}) {
210
- const proxyUrl = getProxyUrl();
211
- if (proxyUrl) {
212
- // Use undici with ProxyAgent when proxy is configured
213
- const proxyAgent = new ProxyAgent(proxyUrl);
214
- const response = yield undiciFetch(url, Object.assign(Object.assign({}, options), { dispatcher: proxyAgent }));
215
- // Cast to native Response type for compatibility
216
- return response;
217
- }
218
- else {
219
- // Use native fetch when no proxy is configured
220
- return fetch(url, options);
221
- }
222
- });
223
- }
224
- /**
225
- * Validates an array of message objects for chat completion tools.
226
- * Ensures each message has a valid role and content field.
227
- *
228
- * @param {any} messages - The messages to validate
229
- * @param {string} toolName - The name of the tool calling this validation (for error messages)
230
- * @throws {Error} If messages is not an array or if any message is invalid
231
- */
232
- function validateMessages(messages, toolName) {
233
- if (!Array.isArray(messages)) {
234
- throw new Error(`Invalid arguments for ${toolName}: 'messages' must be an array`);
235
- }
236
- for (let i = 0; i < messages.length; i++) {
237
- const msg = messages[i];
238
- if (!msg || typeof msg !== 'object') {
239
- throw new Error(`Invalid message at index ${i}: must be an object`);
240
- }
241
- if (!msg.role || typeof msg.role !== 'string') {
242
- throw new Error(`Invalid message at index ${i}: 'role' must be a string`);
243
- }
244
- if (msg.content === undefined || msg.content === null || typeof msg.content !== 'string') {
245
- throw new Error(`Invalid message at index ${i}: 'content' must be a string`);
246
- }
247
- }
248
- }
249
- /**
250
- * Strips thinking tokens (content within <think>...</think> tags) from the response.
251
- * This helps reduce context usage when the thinking process is not needed.
252
- *
253
- * @param {string} content - The content to process
254
- * @returns {string} The content with thinking tokens removed
255
- */
256
- function stripThinkingTokens(content) {
257
- return content.replace(/<think>[\s\S]*?<\/think>/g, '').trim();
258
- }
259
- /**
260
- * Performs a chat completion by sending a request to the Perplexity API.
261
- * Appends citations to the returned message content if they exist.
262
- *
263
- * @param {Array<{ role: string; content: string }>} messages - An array of message objects.
264
- * @param {string} model - The model to use for the completion.
265
- * @param {boolean} stripThinking - If true, removes <think>...</think> tags from the response.
266
- * @returns {Promise<string>} The chat completion result with appended citations.
267
- * @throws Will throw an error if the API request fails.
268
- */
269
- export function performChatCompletion(messages_1) {
270
- return __awaiter(this, arguments, void 0, function* (messages, model = "sonar-pro", stripThinking = false) {
271
- // Read timeout fresh each time to respect env var changes
272
- const TIMEOUT_MS = parseInt(process.env.PERPLEXITY_TIMEOUT_MS || "300000", 10);
273
- // Construct the API endpoint URL and request body
274
- const url = new URL("https://api.perplexity.ai/chat/completions");
275
- const body = {
276
- model: model, // Model identifier passed as parameter
277
- messages: messages,
278
- // Additional parameters can be added here if required (e.g., max_tokens, temperature, etc.)
279
- // See the Sonar API documentation for more details:
280
- // https://docs.perplexity.ai/api-reference/chat-completions
281
- };
282
- const controller = new AbortController();
283
- const timeoutId = setTimeout(() => controller.abort(), TIMEOUT_MS);
284
- let response;
285
- try {
286
- response = yield proxyAwareFetch(url.toString(), {
287
- method: "POST",
288
- headers: {
289
- "Content-Type": "application/json",
290
- "Authorization": `Bearer ${PERPLEXITY_API_KEY}`,
291
- },
292
- body: JSON.stringify(body),
293
- signal: controller.signal,
294
- });
295
- clearTimeout(timeoutId);
296
- }
297
- catch (error) {
298
- clearTimeout(timeoutId);
299
- if (error instanceof Error && error.name === "AbortError") {
300
- throw new Error(`Request timeout: Perplexity API did not respond within ${TIMEOUT_MS}ms. Consider increasing PERPLEXITY_TIMEOUT_MS.`);
301
- }
302
- throw new Error(`Network error while calling Perplexity API: ${error}`);
303
- }
304
- // Check for non-successful HTTP status
305
- if (!response.ok) {
306
- let errorText;
307
- try {
308
- errorText = yield response.text();
309
- }
310
- catch (parseError) {
311
- errorText = "Unable to parse error response";
312
- }
313
- throw new Error(`Perplexity API error: ${response.status} ${response.statusText}\n${errorText}`);
314
- }
315
- // Attempt to parse the JSON response from the API
316
- let data;
317
- try {
318
- data = yield response.json();
319
- }
320
- catch (jsonError) {
321
- throw new Error(`Failed to parse JSON response from Perplexity API: ${jsonError}`);
322
- }
323
- // Validate response structure
324
- if (!data.choices || !Array.isArray(data.choices) || data.choices.length === 0) {
325
- throw new Error("Invalid API response: missing or empty choices array");
326
- }
327
- const firstChoice = data.choices[0];
328
- if (!firstChoice.message || typeof firstChoice.message.content !== 'string') {
329
- throw new Error("Invalid API response: missing message content");
330
- }
331
- // Directly retrieve the main message content from the response
332
- let messageContent = firstChoice.message.content;
333
- // Strip thinking tokens if requested
334
- if (stripThinking) {
335
- messageContent = stripThinkingTokens(messageContent);
336
- }
337
- // If citations are provided, append them to the message content
338
- if (data.citations && Array.isArray(data.citations) && data.citations.length > 0) {
339
- messageContent += "\n\nCitations:\n";
340
- data.citations.forEach((citation, index) => {
341
- messageContent += `[${index + 1}] ${citation}\n`;
342
- });
343
- }
344
- return messageContent;
345
- });
346
- }
347
- /**
348
- * Formats search results from the Perplexity Search API into a readable string.
349
- *
350
- * @param {any} data - The search response data from the API.
351
- * @returns {string} Formatted search results.
352
- */
353
- export function formatSearchResults(data) {
354
- if (!data.results || !Array.isArray(data.results)) {
355
- return "No search results found.";
356
- }
357
- let formattedResults = `Found ${data.results.length} search results:\n\n`;
358
- data.results.forEach((result, index) => {
359
- formattedResults += `${index + 1}. **${result.title}**\n`;
360
- formattedResults += ` URL: ${result.url}\n`;
361
- if (result.snippet) {
362
- formattedResults += ` ${result.snippet}\n`;
363
- }
364
- if (result.date) {
365
- formattedResults += ` Date: ${result.date}\n`;
366
- }
367
- formattedResults += `\n`;
368
- });
369
- return formattedResults;
370
- }
371
- /**
372
- * Performs a web search using the Perplexity Search API.
373
- *
374
- * @param {string} query - The search query string.
375
- * @param {number} maxResults - Maximum number of results to return (1-20).
376
- * @param {number} maxTokensPerPage - Maximum tokens to extract per webpage.
377
- * @param {string} country - Optional ISO country code for regional results.
378
- * @returns {Promise<string>} The formatted search results.
379
- * @throws Will throw an error if the API request fails.
380
- */
381
- export function performSearch(query_1) {
382
- return __awaiter(this, arguments, void 0, function* (query, maxResults = 10, maxTokensPerPage = 1024, country) {
383
- // Read timeout fresh each time to respect env var changes
384
- const TIMEOUT_MS = parseInt(process.env.PERPLEXITY_TIMEOUT_MS || "300000", 10);
385
- const url = new URL("https://api.perplexity.ai/search");
386
- const body = {
387
- query: query,
388
- max_results: maxResults,
389
- max_tokens_per_page: maxTokensPerPage,
390
- };
391
- if (country) {
392
- body.country = country;
393
- }
394
- const controller = new AbortController();
395
- const timeoutId = setTimeout(() => controller.abort(), TIMEOUT_MS);
396
- let response;
397
- try {
398
- response = yield proxyAwareFetch(url.toString(), {
399
- method: "POST",
400
- headers: {
401
- "Content-Type": "application/json",
402
- "Authorization": `Bearer ${PERPLEXITY_API_KEY}`,
403
- },
404
- body: JSON.stringify(body),
405
- signal: controller.signal,
406
- });
407
- clearTimeout(timeoutId);
408
- }
409
- catch (error) {
410
- clearTimeout(timeoutId);
411
- if (error instanceof Error && error.name === "AbortError") {
412
- throw new Error(`Request timeout: Perplexity Search API did not respond within ${TIMEOUT_MS}ms. Consider increasing PERPLEXITY_TIMEOUT_MS.`);
413
- }
414
- throw new Error(`Network error while calling Perplexity Search API: ${error}`);
415
- }
416
- // Check for non-successful HTTP status
417
- if (!response.ok) {
418
- let errorText;
419
- try {
420
- errorText = yield response.text();
421
- }
422
- catch (parseError) {
423
- errorText = "Unable to parse error response";
424
- }
425
- throw new Error(`Perplexity Search API error: ${response.status} ${response.statusText}\n${errorText}`);
426
- }
427
- let data;
428
- try {
429
- data = yield response.json();
430
- }
431
- catch (jsonError) {
432
- throw new Error(`Failed to parse JSON response from Perplexity Search API: ${jsonError}`);
433
- }
434
- return formatSearchResults(data);
435
- });
436
- }
437
- // Initialize the server with tool metadata and capabilities
438
- const server = new Server({
439
- name: "io.github.perplexityai/mcp-server",
440
- version: "0.4.1",
441
- }, {
442
- capabilities: {
443
- tools: {},
444
- },
445
- instructions: `You are the Perplexity MCP Server. Use these tools appropriately:
446
-
447
- - perplexity_search: For quick web searches when you need current information or facts. Returns ranked search results.
448
-
449
- - perplexity_ask: For general questions and conversational queries with real-time web search using the sonar-pro model.
450
-
451
- - perplexity_research: For deep, comprehensive research requiring thorough analysis using the sonar-deep-research model. Use this for complex topics that require detailed investigation.
452
-
453
- - perplexity_reason: For complex analytical tasks requiring advanced reasoning using the sonar-reasoning-pro model. Use this for logical problems, analysis, and decision-making.
454
-
455
- When using perplexity_research or perplexity_reason, consider setting strip_thinking=true to save context tokens if the reasoning process isn't needed in the final output.`,
456
- });
457
- /**
458
- * Registers a handler for listing available tools.
459
- * When the client requests a list of tools, this handler returns all available Perplexity tools.
460
- */
461
- server.setRequestHandler(ListToolsRequestSchema, () => __awaiter(void 0, void 0, void 0, function* () {
462
- return ({
463
- tools: [PERPLEXITY_ASK_TOOL, PERPLEXITY_RESEARCH_TOOL, PERPLEXITY_REASON_TOOL, PERPLEXITY_SEARCH_TOOL],
464
- });
465
- }));
466
- /**
467
- * Registers a handler for calling a specific tool.
468
- * Processes requests by validating input and invoking the appropriate tool.
469
- *
470
- * @param {object} request - The incoming tool call request.
471
- * @returns {Promise<object>} The response containing the tool's result or an error.
11
+ * Initializes and runs the server using standard I/O for communication.
12
+ * Logs an error and exits if the server fails to start.
472
13
  */
473
- server.setRequestHandler(CallToolRequestSchema, (request) => __awaiter(void 0, void 0, void 0, function* () {
14
+ async function main() {
474
15
  try {
475
- const { name, arguments: args } = request.params;
476
- if (!args) {
477
- throw new Error("No arguments provided");
478
- }
479
- switch (name) {
480
- case "perplexity_ask": {
481
- validateMessages(args.messages, "perplexity_ask");
482
- const messages = args.messages;
483
- const result = yield performChatCompletion(messages, "sonar-pro");
484
- return {
485
- content: [{ type: "text", text: result }],
486
- isError: false,
487
- };
488
- }
489
- case "perplexity_research": {
490
- validateMessages(args.messages, "perplexity_research");
491
- const messages = args.messages;
492
- const stripThinking = typeof args.strip_thinking === "boolean" ? args.strip_thinking : false;
493
- const result = yield performChatCompletion(messages, "sonar-deep-research", stripThinking);
494
- return {
495
- content: [{ type: "text", text: result }],
496
- isError: false,
497
- };
498
- }
499
- case "perplexity_reason": {
500
- validateMessages(args.messages, "perplexity_reason");
501
- const messages = args.messages;
502
- const stripThinking = typeof args.strip_thinking === "boolean" ? args.strip_thinking : false;
503
- const result = yield performChatCompletion(messages, "sonar-reasoning-pro", stripThinking);
504
- return {
505
- content: [{ type: "text", text: result }],
506
- isError: false,
507
- };
508
- }
509
- case "perplexity_search": {
510
- if (typeof args.query !== "string") {
511
- throw new Error("Invalid arguments for perplexity_search: 'query' must be a string");
512
- }
513
- const { query, max_results, max_tokens_per_page, country } = args;
514
- const maxResults = typeof max_results === "number" ? max_results : undefined;
515
- const maxTokensPerPage = typeof max_tokens_per_page === "number" ? max_tokens_per_page : undefined;
516
- const countryCode = typeof country === "string" ? country : undefined;
517
- const result = yield performSearch(query, maxResults, maxTokensPerPage, countryCode);
518
- return {
519
- content: [{ type: "text", text: result }],
520
- isError: false,
521
- };
522
- }
523
- default:
524
- // Respond with an error if an unknown tool is requested
525
- return {
526
- content: [{ type: "text", text: `Unknown tool: ${name}` }],
527
- isError: true,
528
- };
529
- }
16
+ const server = createPerplexityServer();
17
+ const transport = new StdioServerTransport();
18
+ await server.connect(transport);
530
19
  }
531
20
  catch (error) {
532
- // Return error details in the response
533
- return {
534
- content: [
535
- {
536
- type: "text",
537
- text: `Error: ${error instanceof Error ? error.message : String(error)}`,
538
- },
539
- ],
540
- isError: true,
541
- };
21
+ console.error("Fatal error running server:", error);
22
+ process.exit(1);
542
23
  }
543
- }));
544
- /**
545
- * Initializes and runs the server using standard I/O for communication.
546
- * Logs an error and exits if the server fails to start.
547
- */
548
- function runServer() {
549
- return __awaiter(this, void 0, void 0, function* () {
550
- try {
551
- const transport = new StdioServerTransport();
552
- yield server.connect(transport);
553
- console.error("Perplexity MCP Server running on stdio with Ask, Research, Reason, and Search tools");
554
- }
555
- catch (error) {
556
- console.error("Fatal error running server:", error);
557
- process.exit(1);
558
- }
559
- });
560
24
  }
561
25
  // Start the server and catch any startup errors
562
- runServer().catch((error) => {
26
+ main().catch((error) => {
563
27
  console.error("Fatal error running server:", error);
564
28
  process.exit(1);
565
29
  });