@perplexity-ai/mcp-server 0.5.1 → 0.5.2

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.5.1"
9
+ "version": "0.5.2"
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.5.1",
16
+ "version": "0.5.2",
17
17
  "author": {
18
18
  "name": "Perplexity AI",
19
19
  "email": "api@perplexity.ai"
package/README.md CHANGED
@@ -1,8 +1,8 @@
1
1
  # Perplexity API Platform MCP Server
2
2
 
3
- [![Install in Cursor](https://custom-icon-badges.demolab.com/badge/Install_in_Cursor-000000?style=for-the-badge&logo=cursor-ai-white)](https://cursor.com/en/install-mcp?name=perplexity&config=eyJ0eXBlIjoic3RkaW8iLCJjb21tYW5kIjoibnB4IiwiYXJncyI6WyIteSIsIkBwZXJwbGV4aXR5LWFpL21jcC1zZXJ2ZXIiXX0=)
3
+ [![Install in Cursor](https://custom-icon-badges.demolab.com/badge/Install_in_Cursor-000000?style=for-the-badge&logo=cursor-ai-white)](https://cursor.com/en/install-mcp?name=perplexity&config=eyJ0eXBlIjoic3RkaW8iLCJjb21tYW5kIjoibnB4IiwiYXJncyI6WyIteSIsIkBwZXJwbGV4aXR5LWFpL21jcC1zZXJ2ZXIiXSwiZW52Ijp7IlBFUlBMRVhJVFlfQVBJX0tFWSI6IiJ9fQ==)
4
4
   
5
- [![Install in VS Code](https://custom-icon-badges.demolab.com/badge/Install_in_VS_Code-007ACC?style=for-the-badge&logo=vsc&logoColor=white)](https://vscode.dev/redirect/mcp/install?name=perplexity&config=%7B%22type%22%3A%22stdio%22%2C%22command%22%3A%22npx%22%2C%22args%22%3A%5B%22-y%22%2C%22%40perplexity-ai%2Fmcp-server%22%5D%7D)
5
+ [![Install in VS Code](https://custom-icon-badges.demolab.com/badge/Install_in_VS_Code-007ACC?style=for-the-badge&logo=vsc&logoColor=white)](https://vscode.dev/redirect/mcp/install?name=perplexity&config=%7B%22type%22%3A%22stdio%22%2C%22command%22%3A%22npx%22%2C%22args%22%3A%5B%22-y%22%2C%22%40perplexity-ai%2Fmcp-server%22%5D%2C%22env%22%3A%7B%22PERPLEXITY_API_KEY%22%3A%22%22%7D%7D)
6
6
   
7
7
  [![npm version](https://img.shields.io/npm/v/%40perplexity-ai%2Fmcp-server?style=for-the-badge&logo=npm&logoColor=white&color=CB3837)](https://www.npmjs.com/package/@perplexity-ai/mcp-server)
8
8
 
@@ -33,6 +33,7 @@ Advanced reasoning and problem-solving using the `sonar-reasoning-pro` model. Pe
33
33
  1. Get your Perplexity API Key from the [API Portal](https://www.perplexity.ai/account/api/group)
34
34
  2. Set it as an environment variable: `PERPLEXITY_API_KEY=your_key_here`
35
35
  3. (Optional) Set a timeout for requests: `PERPLEXITY_TIMEOUT_MS=600000`. The default is 5 minutes.
36
+ 4. (Optional) Set log level for debugging: `PERPLEXITY_LOG_LEVEL=DEBUG|INFO|WARN|ERROR`. The default is ERROR.
36
37
 
37
38
  ### Claude Code
38
39
 
@@ -81,9 +82,9 @@ Or add to your `claude.json`:
81
82
  }
82
83
  ```
83
84
 
84
- ### Cursor / VS Code
85
+ ### Cursor
85
86
 
86
- Add to your `mcp.json` (Cursor) or `.vscode/mcp.json` (VS Code):
87
+ Add to your `mcp.json` (Cursor):
87
88
 
88
89
  ```json
89
90
  {
@@ -92,14 +93,35 @@ Add to your `mcp.json` (Cursor) or `.vscode/mcp.json` (VS Code):
92
93
  "command": "npx",
93
94
  "args": ["-y", "@perplexity-ai/mcp-server"],
94
95
  "env": {
95
- "PERPLEXITY_API_KEY": "your_key_here",
96
- "PERPLEXITY_TIMEOUT_MS": "600000"
96
+ "PERPLEXITY_API_KEY": "your_key_here"
97
97
  }
98
98
  }
99
99
  }
100
100
  }
101
101
  ```
102
102
 
103
+ ### VS Code
104
+
105
+ Add to your `.vscode/mcp.json` (VS Code):
106
+
107
+ ```json
108
+ {
109
+ "servers": {
110
+ "perplexity": {
111
+ "type": "stdio",
112
+ "command": "npx",
113
+ "args": [
114
+ "-y",
115
+ "@perplexity-ai/mcp-server"
116
+ ],
117
+ "env": {
118
+ "PERPLEXITY_API_KEY": "your_key_here"
119
+ }
120
+ }
121
+ }
122
+ }
123
+ ```
124
+
103
125
  Or use the one-click install badges at the top of this README.
104
126
 
105
127
  ### Codex
package/dist/http.js CHANGED
@@ -3,10 +3,11 @@ import express from "express";
3
3
  import cors from "cors";
4
4
  import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
5
5
  import { createPerplexityServer } from "./server.js";
6
+ import { logger } from "./logger.js";
6
7
  // Check for required API key
7
8
  const PERPLEXITY_API_KEY = process.env.PERPLEXITY_API_KEY;
8
9
  if (!PERPLEXITY_API_KEY) {
9
- console.error("Error: PERPLEXITY_API_KEY environment variable is required");
10
+ logger.error("PERPLEXITY_API_KEY environment variable is required");
10
11
  process.exit(1);
11
12
  }
12
13
  const app = express();
@@ -53,7 +54,7 @@ app.all("/mcp", async (req, res) => {
53
54
  await transport.handleRequest(req, res, req.body);
54
55
  }
55
56
  catch (error) {
56
- console.error("Error handling MCP request:", error);
57
+ logger.error("Error handling MCP request", { error: String(error) });
57
58
  if (!res.headersSent) {
58
59
  res.status(500).json({
59
60
  jsonrpc: "2.0",
@@ -73,9 +74,9 @@ app.get("/health", (req, res) => {
73
74
  * Start the HTTP server
74
75
  */
75
76
  app.listen(PORT, BIND_ADDRESS, () => {
76
- console.log(`Perplexity MCP Server listening on http://${BIND_ADDRESS}:${PORT}/mcp`);
77
- console.log(`Allowed origins: ${ALLOWED_ORIGINS.join(", ")}`);
77
+ logger.info(`Perplexity MCP Server listening on http://${BIND_ADDRESS}:${PORT}/mcp`);
78
+ logger.info(`Allowed origins: ${ALLOWED_ORIGINS.join(", ")}`);
78
79
  }).on("error", (error) => {
79
- console.error("Server error:", error);
80
+ logger.error("Server error", { error: String(error) });
80
81
  process.exit(1);
81
82
  });
package/dist/logger.js ADDED
@@ -0,0 +1,82 @@
1
+ /**
2
+ * Simple structured logger for the Perplexity MCP Server
3
+ * Outputs to stderr to avoid interfering with STDIO transport
4
+ */
5
+ export var LogLevel;
6
+ (function (LogLevel) {
7
+ LogLevel[LogLevel["DEBUG"] = 0] = "DEBUG";
8
+ LogLevel[LogLevel["INFO"] = 1] = "INFO";
9
+ LogLevel[LogLevel["WARN"] = 2] = "WARN";
10
+ LogLevel[LogLevel["ERROR"] = 3] = "ERROR";
11
+ })(LogLevel || (LogLevel = {}));
12
+ const LOG_LEVEL_NAMES = {
13
+ [LogLevel.DEBUG]: "DEBUG",
14
+ [LogLevel.INFO]: "INFO",
15
+ [LogLevel.WARN]: "WARN",
16
+ [LogLevel.ERROR]: "ERROR",
17
+ };
18
+ /**
19
+ * Gets the configured log level from environment variable
20
+ * Defaults to ERROR to minimize noise in production
21
+ */
22
+ function getLogLevel() {
23
+ const level = process.env.PERPLEXITY_LOG_LEVEL?.toUpperCase();
24
+ switch (level) {
25
+ case "DEBUG":
26
+ return LogLevel.DEBUG;
27
+ case "INFO":
28
+ return LogLevel.INFO;
29
+ case "WARN":
30
+ return LogLevel.WARN;
31
+ case "ERROR":
32
+ return LogLevel.ERROR;
33
+ default:
34
+ return LogLevel.ERROR;
35
+ }
36
+ }
37
+ const currentLogLevel = getLogLevel();
38
+ function safeStringify(obj) {
39
+ try {
40
+ return JSON.stringify(obj);
41
+ }
42
+ catch {
43
+ return "[Unstringifiable]";
44
+ }
45
+ }
46
+ /**
47
+ * Formats a log message with timestamp and level
48
+ */
49
+ function formatMessage(level, message, meta) {
50
+ const timestamp = new Date().toISOString();
51
+ const levelName = LOG_LEVEL_NAMES[level];
52
+ if (meta && Object.keys(meta).length > 0) {
53
+ return `[${timestamp}] ${levelName}: ${message} ${safeStringify(meta)}`;
54
+ }
55
+ return `[${timestamp}] ${levelName}: ${message}`;
56
+ }
57
+ /**
58
+ * Logs a message if the configured log level allows it
59
+ */
60
+ function log(level, message, meta) {
61
+ if (level >= currentLogLevel) {
62
+ const formatted = formatMessage(level, message, meta);
63
+ console.error(formatted); // Use stderr to avoid interfering with STDIO
64
+ }
65
+ }
66
+ /**
67
+ * Structured logger interface
68
+ */
69
+ export const logger = {
70
+ debug(message, meta) {
71
+ log(LogLevel.DEBUG, message, meta);
72
+ },
73
+ info(message, meta) {
74
+ log(LogLevel.INFO, message, meta);
75
+ },
76
+ warn(message, meta) {
77
+ log(LogLevel.WARN, message, meta);
78
+ },
79
+ error(message, meta) {
80
+ log(LogLevel.ERROR, message, meta);
81
+ },
82
+ };
package/dist/server.js CHANGED
@@ -1,6 +1,7 @@
1
1
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
2
  import { z } from "zod";
3
3
  import { fetch as undiciFetch, ProxyAgent } from "undici";
4
+ import { ChatCompletionResponseSchema, SearchResponseSchema } from "./validation.js";
4
5
  // Retrieve the Perplexity API key from environment variables
5
6
  const PERPLEXITY_API_KEY = process.env.PERPLEXITY_API_KEY;
6
7
  /**
@@ -9,7 +10,7 @@ const PERPLEXITY_API_KEY = process.env.PERPLEXITY_API_KEY;
9
10
  *
10
11
  * @returns {string | undefined} The proxy URL if configured, undefined otherwise
11
12
  */
12
- function getProxyUrl() {
13
+ export function getProxyUrl() {
13
14
  return process.env.PERPLEXITY_PROXY ||
14
15
  process.env.HTTPS_PROXY ||
15
16
  process.env.HTTP_PROXY ||
@@ -23,32 +24,31 @@ function getProxyUrl() {
23
24
  * @param {RequestInit} options - Fetch options
24
25
  * @returns {Promise<Response>} The fetch response
25
26
  */
26
- async function proxyAwareFetch(url, options = {}) {
27
+ export async function proxyAwareFetch(url, options = {}) {
27
28
  const proxyUrl = getProxyUrl();
28
29
  if (proxyUrl) {
29
30
  // Use undici with ProxyAgent when proxy is configured
30
31
  const proxyAgent = new ProxyAgent(proxyUrl);
31
- const response = await undiciFetch(url, {
32
+ const undiciOptions = {
32
33
  ...options,
33
34
  dispatcher: proxyAgent,
34
- });
35
+ };
36
+ const response = await undiciFetch(url, undiciOptions);
35
37
  // Cast to native Response type for compatibility
36
38
  return response;
37
39
  }
38
- else {
39
- // Use native fetch when no proxy is configured
40
- return fetch(url, options);
41
- }
40
+ // Use native fetch when no proxy is configured
41
+ return fetch(url, options);
42
42
  }
43
43
  /**
44
44
  * Validates an array of message objects for chat completion tools.
45
45
  * Ensures each message has a valid role and content field.
46
46
  *
47
- * @param {any} messages - The messages to validate
47
+ * @param {unknown} messages - The messages to validate
48
48
  * @param {string} toolName - The name of the tool calling this validation (for error messages)
49
49
  * @throws {Error} If messages is not an array or if any message is invalid
50
50
  */
51
- function validateMessages(messages, toolName) {
51
+ export function validateMessages(messages, toolName) {
52
52
  if (!Array.isArray(messages)) {
53
53
  throw new Error(`Invalid arguments for ${toolName}: 'messages' must be an array`);
54
54
  }
@@ -72,14 +72,14 @@ function validateMessages(messages, toolName) {
72
72
  * @param {string} content - The content to process
73
73
  * @returns {string} The content with thinking tokens removed
74
74
  */
75
- function stripThinkingTokens(content) {
75
+ export function stripThinkingTokens(content) {
76
76
  return content.replace(/<think>[\s\S]*?<\/think>/g, '').trim();
77
77
  }
78
78
  /**
79
79
  * Performs a chat completion by sending a request to the Perplexity API.
80
80
  * Appends citations to the returned message content if they exist.
81
81
  *
82
- * @param {Array<{ role: string; content: string }>} messages - An array of message objects.
82
+ * @param {Message[]} messages - An array of message objects.
83
83
  * @param {string} model - The model to use for the completion.
84
84
  * @param {boolean} stripThinking - If true, removes <think>...</think> tags from the response.
85
85
  * @returns {Promise<string>} The chat completion result with appended citations.
@@ -130,22 +130,24 @@ export async function performChatCompletion(messages, model = "sonar-pro", strip
130
130
  }
131
131
  throw new Error(`Perplexity API error: ${response.status} ${response.statusText}\n${errorText}`);
132
132
  }
133
- // Attempt to parse the JSON response from the API
134
133
  let data;
135
134
  try {
136
- data = await response.json();
135
+ const json = await response.json();
136
+ data = ChatCompletionResponseSchema.parse(json);
137
137
  }
138
- catch (jsonError) {
139
- throw new Error(`Failed to parse JSON response from Perplexity API: ${jsonError}`);
140
- }
141
- // Validate response structure
142
- if (!data.choices || !Array.isArray(data.choices) || data.choices.length === 0) {
143
- throw new Error("Invalid API response: missing or empty choices array");
138
+ catch (error) {
139
+ if (error instanceof z.ZodError) {
140
+ const issues = error.issues;
141
+ if (issues.some(i => i.path.includes('message') || i.path.includes('content'))) {
142
+ throw new Error("Invalid API response: missing message content");
143
+ }
144
+ if (issues.some(i => i.path.includes('choices'))) {
145
+ throw new Error("Invalid API response: missing or empty choices array");
146
+ }
147
+ }
148
+ throw new Error(`Failed to parse JSON response from Perplexity API: ${error}`);
144
149
  }
145
150
  const firstChoice = data.choices[0];
146
- if (!firstChoice.message || typeof firstChoice.message.content !== 'string') {
147
- throw new Error("Invalid API response: missing message content");
148
- }
149
151
  // Directly retrieve the main message content from the response
150
152
  let messageContent = firstChoice.message.content;
151
153
  // Strip thinking tokens if requested
@@ -164,7 +166,7 @@ export async function performChatCompletion(messages, model = "sonar-pro", strip
164
166
  /**
165
167
  * Formats search results from the Perplexity Search API into a readable string.
166
168
  *
167
- * @param {any} data - The search response data from the API.
169
+ * @param {SearchResponse} data - The search response data from the API.
168
170
  * @returns {string} Formatted search results.
169
171
  */
170
172
  export function formatSearchResults(data) {
@@ -206,10 +208,8 @@ export async function performSearch(query, maxResults = 10, maxTokensPerPage = 1
206
208
  query: query,
207
209
  max_results: maxResults,
208
210
  max_tokens_per_page: maxTokensPerPage,
211
+ ...(country && { country }),
209
212
  };
210
- if (country) {
211
- body.country = country;
212
- }
213
213
  const controller = new AbortController();
214
214
  const timeoutId = setTimeout(() => controller.abort(), TIMEOUT_MS);
215
215
  let response;
@@ -245,10 +245,11 @@ export async function performSearch(query, maxResults = 10, maxTokensPerPage = 1
245
245
  }
246
246
  let data;
247
247
  try {
248
- data = await response.json();
248
+ const json = await response.json();
249
+ data = SearchResponseSchema.parse(json);
249
250
  }
250
- catch (jsonError) {
251
- throw new Error(`Failed to parse JSON response from Perplexity Search API: ${jsonError}`);
251
+ catch (error) {
252
+ throw new Error(`Failed to parse JSON response from Perplexity Search API: ${error}`);
252
253
  }
253
254
  return formatSearchResults(data);
254
255
  }
@@ -261,7 +262,7 @@ export async function performSearch(query, maxResults = 10, maxTokensPerPage = 1
261
262
  export function createPerplexityServer() {
262
263
  const server = new McpServer({
263
264
  name: "io.github.perplexityai/mcp-server",
264
- version: "0.5.1",
265
+ version: "0.5.2",
265
266
  });
266
267
  // Register perplexity_ask tool
267
268
  server.registerTool("perplexity_ask", {
package/dist/types.js ADDED
@@ -0,0 +1,4 @@
1
+ /**
2
+ * Type definitions for the Perplexity MCP Server
3
+ */
4
+ export {};
@@ -0,0 +1,38 @@
1
+ import { z } from "zod";
2
+ export const ChatMessageSchema = z.object({
3
+ content: z.string(),
4
+ role: z.string().optional(),
5
+ });
6
+ export const ChatChoiceSchema = z.object({
7
+ message: ChatMessageSchema,
8
+ finish_reason: z.string().optional(),
9
+ index: z.number().optional(),
10
+ });
11
+ export const TokenUsageSchema = z.object({
12
+ prompt_tokens: z.number().optional(),
13
+ completion_tokens: z.number().optional(),
14
+ total_tokens: z.number().optional(),
15
+ });
16
+ export const ChatCompletionResponseSchema = z.object({
17
+ choices: z.array(ChatChoiceSchema).min(1),
18
+ citations: z.array(z.string()).optional(),
19
+ usage: TokenUsageSchema.optional(),
20
+ id: z.string().optional(),
21
+ model: z.string().optional(),
22
+ created: z.number().optional(),
23
+ });
24
+ export const SearchResultSchema = z.object({
25
+ title: z.string(),
26
+ url: z.string(),
27
+ snippet: z.string().optional(),
28
+ date: z.string().optional(),
29
+ score: z.number().optional(),
30
+ });
31
+ export const SearchUsageSchema = z.object({
32
+ tokens: z.number().optional(),
33
+ });
34
+ export const SearchResponseSchema = z.object({
35
+ results: z.array(SearchResultSchema),
36
+ query: z.string().optional(),
37
+ usage: SearchUsageSchema.optional(),
38
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@perplexity-ai/mcp-server",
3
- "version": "0.5.1",
3
+ "version": "0.5.2",
4
4
  "mcpName": "io.github.perplexityai/mcp-server",
5
5
  "description": "Real-time web search, reasoning, and research through Perplexity's API",
6
6
  "keywords": [