@perplexity-ai/mcp-server 0.5.1 → 0.6.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.5.1"
9
+ "version": "0.6.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.5.1",
16
+ "version": "0.6.0",
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
 
@@ -13,7 +13,7 @@ The official MCP server implementation for the Perplexity API Platform, providin
13
13
  ### **perplexity_search**
14
14
  Direct web search using the Perplexity Search API. Returns ranked search results with metadata, perfect for finding current information.
15
15
 
16
- ### **perplexity_ask**
16
+ ### **perplexity_ask**
17
17
  General-purpose conversational AI with real-time web search using the `sonar-pro` model. Great for quick questions and everyday searches.
18
18
 
19
19
  ### **perplexity_research**
@@ -30,60 +30,37 @@ Advanced reasoning and problem-solving using the `sonar-reasoning-pro` model. Pe
30
30
  ## Configuration
31
31
 
32
32
  ### Get Your API Key
33
+
33
34
  1. Get your Perplexity API Key from the [API Portal](https://www.perplexity.ai/account/api/group)
34
- 2. Set it as an environment variable: `PERPLEXITY_API_KEY=your_key_here`
35
- 3. (Optional) Set a timeout for requests: `PERPLEXITY_TIMEOUT_MS=600000`. The default is 5 minutes.
35
+ 2. Replace `your_key_here` in the configurations below with your API key
36
+ 3. (Optional) Set timeout: `PERPLEXITY_TIMEOUT_MS=600000` (default: 5 minutes)
37
+ 4. (Optional) Set log level: `PERPLEXITY_LOG_LEVEL=DEBUG|INFO|WARN|ERROR` (default: ERROR)
36
38
 
37
39
  ### Claude Code
38
40
 
39
- #### Option 1: Install via Plugin (Recommended)
40
-
41
- The easiest way to get started with Perplexity in Claude Code, set your API key:
42
- ```bash
43
- export PERPLEXITY_API_KEY="your_key_here"
44
- ```
45
- Then:
46
41
  ```bash
47
- # Open Claude Code
48
- claude
49
-
50
- # Add the Perplexity marketplace
51
- /plugin marketplace add perplexityai/modelcontextprotocol
52
-
53
- # Install the plugin
54
- /plugin install perplexity
42
+ claude mcp add perplexity --env PERPLEXITY_API_KEY="your_key_here" -- npx -y @perplexity-ai/mcp-server
55
43
  ```
56
44
 
57
- #### Option 2: Manual Configuration
58
-
59
- Run in your terminal:
60
-
45
+ Or install via plugin:
61
46
  ```bash
62
- claude mcp add perplexity --transport stdio --env PERPLEXITY_API_KEY=your_key_here -- npx -y perplexity-mcp
47
+ export PERPLEXITY_API_KEY="your_key_here"
48
+ claude
49
+ # Then run: /plugin marketplace add perplexityai/modelcontextprotocol
50
+ # Then run: /plugin install perplexity
63
51
  ```
64
52
 
65
- Or add to your `claude.json`:
53
+ ### Cursor, Claude Desktop & Windsurf
66
54
 
67
- ```json
68
- "mcpServers": {
69
- "perplexity": {
70
- "type": "stdio",
71
- "command": "npx",
72
- "args": [
73
- "-y",
74
- "perplexity-mcp"
75
- ],
76
- "env": {
77
- "PERPLEXITY_API_KEY": "your_key_here",
78
- "PERPLEXITY_TIMEOUT_MS": "600000"
79
- }
80
- }
81
- }
82
- ```
55
+ We recommend using the one-click install badge at the top of this README for Cursor.
83
56
 
84
- ### Cursor / VS Code
57
+ For manual setup, all these clients use the same `mcpServers` format:
85
58
 
86
- Add to your `mcp.json` (Cursor) or `.vscode/mcp.json` (VS Code):
59
+ | Client | Config File |
60
+ |--------|-------------|
61
+ | Cursor | `~/.cursor/mcp.json` |
62
+ | Claude Desktop | `claude_desktop_config.json` |
63
+ | Windsurf | `~/.codeium/windsurf/mcp_config.json` |
87
64
 
88
65
  ```json
89
66
  {
@@ -92,51 +69,42 @@ Add to your `mcp.json` (Cursor) or `.vscode/mcp.json` (VS Code):
92
69
  "command": "npx",
93
70
  "args": ["-y", "@perplexity-ai/mcp-server"],
94
71
  "env": {
95
- "PERPLEXITY_API_KEY": "your_key_here",
96
- "PERPLEXITY_TIMEOUT_MS": "600000"
72
+ "PERPLEXITY_API_KEY": "your_key_here"
97
73
  }
98
74
  }
99
75
  }
100
76
  }
101
77
  ```
102
78
 
103
- Or use the one-click install badges at the top of this README.
104
-
105
- ### Codex
79
+ ### VS Code
106
80
 
107
- Run in your terminal:
108
-
109
- ```bash
110
- codex mcp add perplexity --env PERPLEXITY_API_KEY=your_key_here -- npx -y @perplexity-ai/mcp-server
111
- ```
112
-
113
- ### Claude Desktop
114
-
115
- Add to your `claude_desktop_config.json`:
81
+ We recommend using the one-click install badge at the top of this README for VS Code, or for manual setup, add to `.vscode/mcp.json`:
116
82
 
117
83
  ```json
118
84
  {
119
- "mcpServers": {
85
+ "servers": {
120
86
  "perplexity": {
87
+ "type": "stdio",
121
88
  "command": "npx",
122
89
  "args": ["-y", "@perplexity-ai/mcp-server"],
123
90
  "env": {
124
- "PERPLEXITY_API_KEY": "your_key_here",
125
- "PERPLEXITY_TIMEOUT_MS": "600000"
91
+ "PERPLEXITY_API_KEY": "your_key_here"
126
92
  }
127
93
  }
128
94
  }
129
95
  }
130
96
  ```
131
97
 
132
- ### Other MCP Clients
133
-
134
- For any MCP-compatible client, use:
98
+ ### Codex
135
99
 
136
100
  ```bash
137
- npx @perplexity-ai/mcp-server
101
+ codex mcp add perplexity --env PERPLEXITY_API_KEY="your_key_here" -- npx -y @perplexity-ai/mcp-server
138
102
  ```
139
103
 
104
+ ### Other MCP Clients
105
+
106
+ Most clients can be manually configured to use the `mcpServers` wrapper in their configuration file (like Cursor). If your client doesn't work, check its documentation for the correct wrapper format.
107
+
140
108
  ### Proxy Setup (For Corporate Networks)
141
109
 
142
110
  If you are running this server at work—especially behind a company firewall or proxy—you may need to tell the program how to send its internet traffic through your network's proxy. Follow these steps:
@@ -168,39 +136,34 @@ If you'd rather use the standard variables, we support `HTTPS_PROXY` and `HTTP_P
168
136
  > The server checks proxy settings in this order: `PERPLEXITY_PROXY` → `HTTPS_PROXY` → `HTTP_PROXY`. If none are set, it connects directly to the internet.
169
137
  > URLs must include `https://`. Typical ports are `8080`, `3128`, and `80`.
170
138
 
171
-
172
139
  ### HTTP Server Deployment
173
140
 
174
- For cloud or shared deployments, you can run the server in HTTP mode:
141
+ For cloud or shared deployments, run the server in HTTP mode.
175
142
 
176
143
  #### Environment Variables
177
144
 
178
- The HTTP server supports these configuration options:
179
-
180
- - **`PORT`** - HTTP server port (default: `8080`)
181
- - **`BIND_ADDRESS`** - Network interface to bind to (default: `127.0.0.1` for local, use `0.0.0.0` for hosted)
182
- - **`ALLOWED_ORIGINS`** - Comma-separated list of allowed CORS origins (default: `http://localhost:3000,http://127.0.0.1:3000`, use `*` for public service)
183
- - **`PERPLEXITY_API_KEY`** - Your Perplexity API key (required)
145
+ | Variable | Description | Default |
146
+ |----------|-------------|---------|
147
+ | `PERPLEXITY_API_KEY` | Your Perplexity API key | *Required* |
148
+ | `PORT` | HTTP server port | `8080` |
149
+ | `BIND_ADDRESS` | Network interface to bind to | `0.0.0.0` |
150
+ | `ALLOWED_ORIGINS` | CORS origins (comma-separated) | `*` |
184
151
 
185
- #### Using Docker
152
+ #### Docker
186
153
 
187
154
  ```bash
188
155
  docker build -t perplexity-mcp-server .
189
156
  docker run -p 8080:8080 -e PERPLEXITY_API_KEY=your_key_here perplexity-mcp-server
190
157
  ```
191
158
 
192
- The server will be accessible at `http://localhost:8080/mcp`
193
-
194
- #### Using Node.js Directly
159
+ #### Node.js
195
160
 
196
161
  ```bash
197
- npm install
198
- npm run build
199
- npm run start:http
162
+ export PERPLEXITY_API_KEY=your_key_here
163
+ npm install && npm run build && npm run start:http
200
164
  ```
201
165
 
202
- Connect your MCP client to: `http://localhost:8080/mcp`
203
-
166
+ The server will be accessible at `http://localhost:8080/mcp`
204
167
 
205
168
  ## Troubleshooting
206
169
 
@@ -209,6 +172,7 @@ Connect your MCP client to: `http://localhost:8080/mcp`
209
172
  - **Tool Not Found**: Make sure the package is installed and the command path is correct
210
173
  - **Timeout Errors**: For very long research queries, set `PERPLEXITY_TIMEOUT_MS` to a higher value
211
174
  - **Proxy Issues**: Verify your `PERPLEXITY_PROXY` or `HTTPS_PROXY` setup and ensure `api.perplexity.ai` isn't blocked by your firewall.
175
+ - **EOF / Initialize Errors**: Some strict MCP clients fail because `npx` writes installation messages to stdout. Use `npx -yq` instead of `npx -y` to suppress this output.
212
176
 
213
177
  For support, visit [community.perplexity.ai](https://community.perplexity.ai) or [file an issue](https://github.com/perplexityai/modelcontextprotocol/issues).
214
178
 
package/dist/http.js CHANGED
@@ -3,19 +3,16 @@ 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
- // Check for required API key
6
+ import { logger } from "./logger.js";
7
7
  const PERPLEXITY_API_KEY = process.env.PERPLEXITY_API_KEY;
8
8
  if (!PERPLEXITY_API_KEY) {
9
- console.error("Error: PERPLEXITY_API_KEY environment variable is required");
9
+ logger.error("PERPLEXITY_API_KEY environment variable is required");
10
10
  process.exit(1);
11
11
  }
12
12
  const app = express();
13
13
  const PORT = parseInt(process.env.PORT || "8080", 10);
14
- const BIND_ADDRESS = process.env.BIND_ADDRESS || "127.0.0.1";
15
- const ALLOWED_ORIGINS = process.env.ALLOWED_ORIGINS?.split(",") || [
16
- "http://localhost:3000",
17
- "http://127.0.0.1:3000",
18
- ];
14
+ const BIND_ADDRESS = process.env.BIND_ADDRESS || "0.0.0.0";
15
+ const ALLOWED_ORIGINS = process.env.ALLOWED_ORIGINS?.split(",") || ["*"];
19
16
  // CORS configuration for browser-based MCP clients
20
17
  app.use(cors({
21
18
  origin: (origin, callback) => {
@@ -36,10 +33,6 @@ app.use(cors({
36
33
  }));
37
34
  app.use(express.json());
38
35
  const mcpServer = createPerplexityServer();
39
- /**
40
- * POST: client-to-server messages (requests, responses, notifications)
41
- * GET: SSE stream for server-to-client messages (notifications, requests)
42
- */
43
36
  app.all("/mcp", async (req, res) => {
44
37
  try {
45
38
  const transport = new StreamableHTTPServerTransport({
@@ -53,7 +46,7 @@ app.all("/mcp", async (req, res) => {
53
46
  await transport.handleRequest(req, res, req.body);
54
47
  }
55
48
  catch (error) {
56
- console.error("Error handling MCP request:", error);
49
+ logger.error("Error handling MCP request", { error: String(error) });
57
50
  if (!res.headersSent) {
58
51
  res.status(500).json({
59
52
  jsonrpc: "2.0",
@@ -63,19 +56,13 @@ app.all("/mcp", async (req, res) => {
63
56
  }
64
57
  }
65
58
  });
66
- /**
67
- * Health check endpoint
68
- */
69
59
  app.get("/health", (req, res) => {
70
60
  res.json({ status: "ok", service: "perplexity-mcp-server" });
71
61
  });
72
- /**
73
- * Start the HTTP server
74
- */
75
62
  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(", ")}`);
63
+ logger.info(`Perplexity MCP Server listening on http://${BIND_ADDRESS}:${PORT}/mcp`);
64
+ logger.info(`Allowed origins: ${ALLOWED_ORIGINS.join(", ")}`);
78
65
  }).on("error", (error) => {
79
- console.error("Server error:", error);
66
+ logger.error("Server error", { error: String(error) });
80
67
  process.exit(1);
81
68
  });
package/dist/index.js CHANGED
@@ -1,19 +1,14 @@
1
1
  #!/usr/bin/env node
2
2
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
3
3
  import { createPerplexityServer } from "./server.js";
4
- // Check for required API key
5
4
  const PERPLEXITY_API_KEY = process.env.PERPLEXITY_API_KEY;
6
5
  if (!PERPLEXITY_API_KEY) {
7
6
  console.error("Error: PERPLEXITY_API_KEY environment variable is required");
8
7
  process.exit(1);
9
8
  }
10
- /**
11
- * Initializes and runs the server using standard I/O for communication.
12
- * Logs an error and exits if the server fails to start.
13
- */
14
9
  async function main() {
15
10
  try {
16
- const server = createPerplexityServer();
11
+ const server = createPerplexityServer("local-mcp");
17
12
  const transport = new StdioServerTransport();
18
13
  await server.connect(transport);
19
14
  }
@@ -22,7 +17,6 @@ async function main() {
22
17
  process.exit(1);
23
18
  }
24
19
  }
25
- // Start the server and catch any startup errors
26
20
  main().catch((error) => {
27
21
  console.error("Fatal error running server:", error);
28
22
  process.exit(1);
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,54 +1,28 @@
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
- // Retrieve the Perplexity API key from environment variables
4
+ import { ChatCompletionResponseSchema, SearchResponseSchema } from "./validation.js";
5
5
  const PERPLEXITY_API_KEY = process.env.PERPLEXITY_API_KEY;
6
- /**
7
- * Gets the proxy URL from environment variables.
8
- * Checks PERPLEXITY_PROXY, HTTPS_PROXY, HTTP_PROXY in order.
9
- *
10
- * @returns {string | undefined} The proxy URL if configured, undefined otherwise
11
- */
12
- function getProxyUrl() {
6
+ export function getProxyUrl() {
13
7
  return process.env.PERPLEXITY_PROXY ||
14
8
  process.env.HTTPS_PROXY ||
15
9
  process.env.HTTP_PROXY ||
16
10
  undefined;
17
11
  }
18
- /**
19
- * Creates a proxy-aware fetch function.
20
- * Uses undici with ProxyAgent when a proxy is configured, otherwise uses native fetch.
21
- *
22
- * @param {string} url - The URL to fetch
23
- * @param {RequestInit} options - Fetch options
24
- * @returns {Promise<Response>} The fetch response
25
- */
26
- async function proxyAwareFetch(url, options = {}) {
12
+ export async function proxyAwareFetch(url, options = {}) {
27
13
  const proxyUrl = getProxyUrl();
28
14
  if (proxyUrl) {
29
- // Use undici with ProxyAgent when proxy is configured
30
15
  const proxyAgent = new ProxyAgent(proxyUrl);
31
- const response = await undiciFetch(url, {
16
+ const undiciOptions = {
32
17
  ...options,
33
18
  dispatcher: proxyAgent,
34
- });
35
- // Cast to native Response type for compatibility
19
+ };
20
+ const response = await undiciFetch(url, undiciOptions);
36
21
  return response;
37
22
  }
38
- else {
39
- // Use native fetch when no proxy is configured
40
- return fetch(url, options);
41
- }
23
+ return fetch(url, options);
42
24
  }
43
- /**
44
- * Validates an array of message objects for chat completion tools.
45
- * Ensures each message has a valid role and content field.
46
- *
47
- * @param {any} messages - The messages to validate
48
- * @param {string} toolName - The name of the tool calling this validation (for error messages)
49
- * @throws {Error} If messages is not an array or if any message is invalid
50
- */
51
- function validateMessages(messages, toolName) {
25
+ export function validateMessages(messages, toolName) {
52
26
  if (!Array.isArray(messages)) {
53
27
  throw new Error(`Invalid arguments for ${toolName}: 'messages' must be an array`);
54
28
  }
@@ -65,33 +39,15 @@ function validateMessages(messages, toolName) {
65
39
  }
66
40
  }
67
41
  }
68
- /**
69
- * Strips thinking tokens (content within <think>...</think> tags) from the response.
70
- * This helps reduce context usage when the thinking process is not needed.
71
- *
72
- * @param {string} content - The content to process
73
- * @returns {string} The content with thinking tokens removed
74
- */
75
- function stripThinkingTokens(content) {
42
+ export function stripThinkingTokens(content) {
76
43
  return content.replace(/<think>[\s\S]*?<\/think>/g, '').trim();
77
44
  }
78
- /**
79
- * Performs a chat completion by sending a request to the Perplexity API.
80
- * Appends citations to the returned message content if they exist.
81
- *
82
- * @param {Array<{ role: string; content: string }>} messages - An array of message objects.
83
- * @param {string} model - The model to use for the completion.
84
- * @param {boolean} stripThinking - If true, removes <think>...</think> tags from the response.
85
- * @returns {Promise<string>} The chat completion result with appended citations.
86
- * @throws Will throw an error if the API request fails.
87
- */
88
- export async function performChatCompletion(messages, model = "sonar-pro", stripThinking = false) {
45
+ export async function performChatCompletion(messages, model = "sonar-pro", stripThinking = false, serviceOrigin) {
89
46
  if (!PERPLEXITY_API_KEY) {
90
47
  throw new Error("PERPLEXITY_API_KEY environment variable is required");
91
48
  }
92
49
  // Read timeout fresh each time to respect env var changes
93
50
  const TIMEOUT_MS = parseInt(process.env.PERPLEXITY_TIMEOUT_MS || "300000", 10);
94
- // Construct the API endpoint URL and request body
95
51
  const url = new URL("https://api.perplexity.ai/chat/completions");
96
52
  const body = {
97
53
  model: model,
@@ -101,12 +57,16 @@ export async function performChatCompletion(messages, model = "sonar-pro", strip
101
57
  const timeoutId = setTimeout(() => controller.abort(), TIMEOUT_MS);
102
58
  let response;
103
59
  try {
60
+ const headers = {
61
+ "Content-Type": "application/json",
62
+ "Authorization": `Bearer ${PERPLEXITY_API_KEY}`,
63
+ };
64
+ if (serviceOrigin) {
65
+ headers["X-Service"] = serviceOrigin;
66
+ }
104
67
  response = await proxyAwareFetch(url.toString(), {
105
68
  method: "POST",
106
- headers: {
107
- "Content-Type": "application/json",
108
- "Authorization": `Bearer ${PERPLEXITY_API_KEY}`,
109
- },
69
+ headers,
110
70
  body: JSON.stringify(body),
111
71
  signal: controller.signal,
112
72
  });
@@ -119,7 +79,6 @@ export async function performChatCompletion(messages, model = "sonar-pro", strip
119
79
  }
120
80
  throw new Error(`Network error while calling Perplexity API: ${error}`);
121
81
  }
122
- // Check for non-successful HTTP status
123
82
  if (!response.ok) {
124
83
  let errorText;
125
84
  try {
@@ -130,29 +89,28 @@ export async function performChatCompletion(messages, model = "sonar-pro", strip
130
89
  }
131
90
  throw new Error(`Perplexity API error: ${response.status} ${response.statusText}\n${errorText}`);
132
91
  }
133
- // Attempt to parse the JSON response from the API
134
92
  let data;
135
93
  try {
136
- data = await response.json();
137
- }
138
- catch (jsonError) {
139
- throw new Error(`Failed to parse JSON response from Perplexity API: ${jsonError}`);
94
+ const json = await response.json();
95
+ data = ChatCompletionResponseSchema.parse(json);
140
96
  }
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");
97
+ catch (error) {
98
+ if (error instanceof z.ZodError) {
99
+ const issues = error.issues;
100
+ if (issues.some(i => i.path.includes('message') || i.path.includes('content'))) {
101
+ throw new Error("Invalid API response: missing message content");
102
+ }
103
+ if (issues.some(i => i.path.includes('choices'))) {
104
+ throw new Error("Invalid API response: missing or empty choices array");
105
+ }
106
+ }
107
+ throw new Error(`Failed to parse JSON response from Perplexity API: ${error}`);
144
108
  }
145
109
  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
- // Directly retrieve the main message content from the response
150
110
  let messageContent = firstChoice.message.content;
151
- // Strip thinking tokens if requested
152
111
  if (stripThinking) {
153
112
  messageContent = stripThinkingTokens(messageContent);
154
113
  }
155
- // If citations are provided, append them to the message content
156
114
  if (data.citations && Array.isArray(data.citations) && data.citations.length > 0) {
157
115
  messageContent += "\n\nCitations:\n";
158
116
  data.citations.forEach((citation, index) => {
@@ -161,12 +119,6 @@ export async function performChatCompletion(messages, model = "sonar-pro", strip
161
119
  }
162
120
  return messageContent;
163
121
  }
164
- /**
165
- * Formats search results from the Perplexity Search API into a readable string.
166
- *
167
- * @param {any} data - The search response data from the API.
168
- * @returns {string} Formatted search results.
169
- */
170
122
  export function formatSearchResults(data) {
171
123
  if (!data.results || !Array.isArray(data.results)) {
172
124
  return "No search results found.";
@@ -185,17 +137,7 @@ export function formatSearchResults(data) {
185
137
  });
186
138
  return formattedResults;
187
139
  }
188
- /**
189
- * Performs a web search using the Perplexity Search API.
190
- *
191
- * @param {string} query - The search query string.
192
- * @param {number} maxResults - Maximum number of results to return (1-20).
193
- * @param {number} maxTokensPerPage - Maximum tokens to extract per webpage.
194
- * @param {string} country - Optional ISO country code for regional results.
195
- * @returns {Promise<string>} The formatted search results.
196
- * @throws Will throw an error if the API request fails.
197
- */
198
- export async function performSearch(query, maxResults = 10, maxTokensPerPage = 1024, country) {
140
+ export async function performSearch(query, maxResults = 10, maxTokensPerPage = 1024, country, serviceOrigin) {
199
141
  if (!PERPLEXITY_API_KEY) {
200
142
  throw new Error("PERPLEXITY_API_KEY environment variable is required");
201
143
  }
@@ -206,20 +148,22 @@ export async function performSearch(query, maxResults = 10, maxTokensPerPage = 1
206
148
  query: query,
207
149
  max_results: maxResults,
208
150
  max_tokens_per_page: maxTokensPerPage,
151
+ ...(country && { country }),
209
152
  };
210
- if (country) {
211
- body.country = country;
212
- }
213
153
  const controller = new AbortController();
214
154
  const timeoutId = setTimeout(() => controller.abort(), TIMEOUT_MS);
215
155
  let response;
216
156
  try {
157
+ const headers = {
158
+ "Content-Type": "application/json",
159
+ "Authorization": `Bearer ${PERPLEXITY_API_KEY}`,
160
+ };
161
+ if (serviceOrigin) {
162
+ headers["X-Service"] = serviceOrigin;
163
+ }
217
164
  response = await proxyAwareFetch(url.toString(), {
218
165
  method: "POST",
219
- headers: {
220
- "Content-Type": "application/json",
221
- "Authorization": `Bearer ${PERPLEXITY_API_KEY}`,
222
- },
166
+ headers,
223
167
  body: JSON.stringify(body),
224
168
  signal: controller.signal,
225
169
  });
@@ -232,7 +176,6 @@ export async function performSearch(query, maxResults = 10, maxTokensPerPage = 1
232
176
  }
233
177
  throw new Error(`Network error while calling Perplexity Search API: ${error}`);
234
178
  }
235
- // Check for non-successful HTTP status
236
179
  if (!response.ok) {
237
180
  let errorText;
238
181
  try {
@@ -245,25 +188,19 @@ export async function performSearch(query, maxResults = 10, maxTokensPerPage = 1
245
188
  }
246
189
  let data;
247
190
  try {
248
- data = await response.json();
191
+ const json = await response.json();
192
+ data = SearchResponseSchema.parse(json);
249
193
  }
250
- catch (jsonError) {
251
- throw new Error(`Failed to parse JSON response from Perplexity Search API: ${jsonError}`);
194
+ catch (error) {
195
+ throw new Error(`Failed to parse JSON response from Perplexity Search API: ${error}`);
252
196
  }
253
197
  return formatSearchResults(data);
254
198
  }
255
- /**
256
- * Creates and configures the Perplexity MCP server with all tools.
257
- * This factory function is transport-agnostic and returns a configured server instance.
258
- *
259
- * @returns The configured MCP server instance
260
- */
261
- export function createPerplexityServer() {
199
+ export function createPerplexityServer(serviceOrigin) {
262
200
  const server = new McpServer({
263
201
  name: "io.github.perplexityai/mcp-server",
264
- version: "0.5.1",
202
+ version: "0.6.0",
265
203
  });
266
- // Register perplexity_ask tool
267
204
  server.registerTool("perplexity_ask", {
268
205
  title: "Ask Perplexity",
269
206
  description: "Engages in a conversation using the Sonar API. " +
@@ -284,13 +221,12 @@ export function createPerplexityServer() {
284
221
  },
285
222
  }, async ({ messages }) => {
286
223
  validateMessages(messages, "perplexity_ask");
287
- const result = await performChatCompletion(messages, "sonar-pro");
224
+ const result = await performChatCompletion(messages, "sonar-pro", false, serviceOrigin);
288
225
  return {
289
226
  content: [{ type: "text", text: result }],
290
227
  structuredContent: { response: result },
291
228
  };
292
229
  });
293
- // Register perplexity_research tool
294
230
  server.registerTool("perplexity_research", {
295
231
  title: "Deep Research",
296
232
  description: "Performs deep research using the Perplexity API. " +
@@ -314,13 +250,12 @@ export function createPerplexityServer() {
314
250
  }, async ({ messages, strip_thinking }) => {
315
251
  validateMessages(messages, "perplexity_research");
316
252
  const stripThinking = typeof strip_thinking === "boolean" ? strip_thinking : false;
317
- const result = await performChatCompletion(messages, "sonar-deep-research", stripThinking);
253
+ const result = await performChatCompletion(messages, "sonar-deep-research", stripThinking, serviceOrigin);
318
254
  return {
319
255
  content: [{ type: "text", text: result }],
320
256
  structuredContent: { response: result },
321
257
  };
322
258
  });
323
- // Register perplexity_reason tool
324
259
  server.registerTool("perplexity_reason", {
325
260
  title: "Advanced Reasoning",
326
261
  description: "Performs reasoning tasks using the Perplexity API. " +
@@ -344,13 +279,12 @@ export function createPerplexityServer() {
344
279
  }, async ({ messages, strip_thinking }) => {
345
280
  validateMessages(messages, "perplexity_reason");
346
281
  const stripThinking = typeof strip_thinking === "boolean" ? strip_thinking : false;
347
- const result = await performChatCompletion(messages, "sonar-reasoning-pro", stripThinking);
282
+ const result = await performChatCompletion(messages, "sonar-reasoning-pro", stripThinking, serviceOrigin);
348
283
  return {
349
284
  content: [{ type: "text", text: result }],
350
285
  structuredContent: { response: result },
351
286
  };
352
287
  });
353
- // Register perplexity_search tool
354
288
  server.registerTool("perplexity_search", {
355
289
  title: "Search the Web",
356
290
  description: "Performs web search using the Perplexity Search API. " +
@@ -376,7 +310,7 @@ export function createPerplexityServer() {
376
310
  const maxResults = typeof max_results === "number" ? max_results : 10;
377
311
  const maxTokensPerPage = typeof max_tokens_per_page === "number" ? max_tokens_per_page : 1024;
378
312
  const countryCode = typeof country === "string" ? country : undefined;
379
- const result = await performSearch(query, maxResults, maxTokensPerPage, countryCode);
313
+ const result = await performSearch(query, maxResults, maxTokensPerPage, countryCode, serviceOrigin);
380
314
  return {
381
315
  content: [{ type: "text", text: result }],
382
316
  structuredContent: { results: result },
package/dist/types.js ADDED
@@ -0,0 +1 @@
1
+ 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.6.0",
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": [